diff --git a/packages/harvest/Cargo.toml b/packages/harvest/Cargo.toml index 80e0f4bb979..1e967944777 100644 --- a/packages/harvest/Cargo.toml +++ b/packages/harvest/Cargo.toml @@ -51,3 +51,35 @@ tokio-postgres = { version = "0.7", features = ["with-uuid-1", "with-chrono-0_4" # used for sanitizing input regex = { version = "1.10" } + +[lints.rustdoc] +missing_crate_level_docs = "deny" +broken_intra_doc_links = "deny" + +[lints.rust] +missing_docs = "deny" +unsafe_code = "forbid" +private_interfaces = "warn" +private_bounds = "warn" +unnameable_types = "warn" +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] } + +[lints.clippy] +missing_docs_in_private_items = "deny" +missing_errors_doc = "deny" +missing_panics_doc = "deny" +doc_markdown = "deny" +unwrap_used = "deny" +panic = "deny" +shadow_unrelated = "deny" +print_stdout = "deny" +print_stderr = "deny" +indexing_slicing = "deny" +missing_const_for_fn = "deny" +future_not_send = "deny" +arithmetic_side_effects = "deny" +suspicious = "deny" +complexity = "deny" +style = "deny" +perf = "deny" +pedantic = "deny" \ No newline at end of file diff --git a/packages/harvest/src/main.rs b/packages/harvest/src/main.rs index cab8879efa4..97615b2310b 100644 --- a/packages/harvest/src/main.rs +++ b/packages/harvest/src/main.rs @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2025 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only +//! Harvest REST API #![recursion_limit = "256"] +#![allow(clippy::too_many_lines)] #[macro_use] extern crate rocket; @@ -14,18 +16,22 @@ use windmill::services::{ probe::{setup_probe, AppName}, }; +/// HTTP routes mod routes; +/// Authorization and database services mod services; +/// Types and data structures mod types; #[launch] +/// Launches the Rocket server. async fn rocket() -> _ { dotenv().ok(); init_log(true); setup_probe(AppName::HARVEST).await; set_is_app_active(true); - init_plugin_manager().await.unwrap(); + init_plugin_manager().await; rocket::build() .register( diff --git a/packages/harvest/src/routes/api_datafix.rs b/packages/harvest/src/routes/api_datafix.rs index d3af4210084..1b22fcd79f8 100644 --- a/packages/harvest/src/routes/api_datafix.rs +++ b/packages/harvest/src/routes/api_datafix.rs @@ -14,8 +14,13 @@ use serde::Serialize; use tracing::{error, info, instrument}; use windmill::services; use windmill::services::database::{get_hasura_pool, get_keycloak_pool}; -use windmill::services::datafix::types::*; +use windmill::services::datafix::types::{ + DatafixResponse, JsonErrorResponse, MarkVotedBody, VoterInformationBody, +}; +/// Adds a new voter to the datafix event. +/// +/// Requires `DATAFIX_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/add-voter", format = "json", data = "")] pub async fn add_voter( @@ -59,6 +64,9 @@ pub async fn add_voter( .await } +/// Updates an existing voter's information in the datafix event. +/// +/// Requires `DATAFIX_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/update-voter", format = "json", data = "")] pub async fn update_voter( @@ -114,11 +122,16 @@ pub async fn update_voter( .await } +/// Request body #[derive(Deserialize, Debug)] pub struct VoterIdBody { + /// The unique identifier of the voter voter_id: String, } +/// Deletes a voter from the datafix event. +/// +/// Requires `DATAFIX_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/delete-voter", format = "json", data = "")] pub async fn delete_voter( @@ -174,6 +187,9 @@ pub async fn delete_voter( .await } +/// Unmarks a voter as voted in the datafix event. +/// +/// Requires `DATAFIX_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/unmark-voted", format = "json", data = "")] pub async fn unmark_voted( @@ -229,6 +245,9 @@ pub async fn unmark_voted( .await } +/// Marks a voter as voted in the datafix event. +/// +/// Requires `DATAFIX_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/mark-voted", format = "json", data = "")] pub async fn mark_voted( @@ -284,11 +303,16 @@ pub async fn mark_voted( .await } +/// Response containing a replaced PIN for a voter. #[derive(Serialize, Debug)] pub struct ReplacePinOutput { + /// The newly generated PIN pin: String, } +/// Generates and replaces a voter's PIN in the datafix event. +/// +/// Requires `DATAFIX_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/replace-pin", format = "json", data = "")] pub async fn replace_pin( @@ -313,23 +337,23 @@ pub async fn replace_pin( let mut hasura_db_client: DbClient = get_hasura_pool().await.get().await.map_err(|e| { - error!("Error getting hasura client {}", e); + error!("Error getting hasura client {e:?}"); DatafixResponse::new(Status::InternalServerError) })?; let hasura_transaction = hasura_db_client.transaction().await.map_err(|e| { - error!("Error starting hasura transaction {}", e); + error!("Error starting hasura transaction {e:?}"); DatafixResponse::new(Status::InternalServerError) })?; let mut keycloak_db_client: DbClient = get_keycloak_pool().await.get().await.map_err(|e| { - error!("Error getting keycloak client {}", e); + error!("Error getting keycloak client {e:?}"); DatafixResponse::new(Status::InternalServerError) })?; let keycloak_transaction = keycloak_db_client.transaction().await.map_err(|e| { - error!("Error starting keycloak transaction {}", e); + error!("Error starting keycloak transaction {e:?}"); DatafixResponse::new(Status::InternalServerError) })?; diff --git a/packages/harvest/src/routes/applications.rs b/packages/harvest/src/routes/applications.rs index e0c34773720..d537e428403 100644 --- a/packages/harvest/src/routes/applications.rs +++ b/packages/harvest/src/routes/applications.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only +#![allow(clippy::large_futures)] use std::collections::HashMap; use std::iter::Map; @@ -34,17 +35,28 @@ use windmill::types::application::{ ApplicationStatus, ApplicationType, ApplicationsError, }; +/// Request body for verifying a user application. #[derive(Deserialize, Debug)] pub struct ApplicationVerifyBody { + /// The ID of the applicant applicant_id: String, + /// The applicant's data as key-value pairs applicant_data: HashMap, + /// The tenant ID tenant_id: String, + /// The election event ID election_event_id: String, + /// Optional area ID area_id: Option, + /// Optional labels for the application labels: Option, + /// Application annotations annotations: ApplicationAnnotations, } +/// Verifies a user application against criteria. +/// +/// Requires `SERVICE_ACCOUNT` permission. #[instrument(skip(claims))] #[post("/verify-application", format = "json", data = "")] pub async fn verify_user_application( @@ -66,7 +78,7 @@ pub async fn verify_user_application( .map_err(|e| { ErrorResponse::new( Status::Unauthorized, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::Unauthorized, ) })?; @@ -75,7 +87,7 @@ pub async fn verify_user_application( get_hasura_pool().await.get().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::InternalServerError, ) })?; @@ -84,7 +96,7 @@ pub async fn verify_user_application( hasura_db_client.transaction().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::GetTransactionFailed, ) })?; @@ -93,7 +105,7 @@ pub async fn verify_user_application( get_keycloak_pool().await.get().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::GetTransactionFailed, ) })?; @@ -101,7 +113,7 @@ pub async fn verify_user_application( keycloak_db_client.transaction().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::GetTransactionFailed, ) })?; @@ -120,12 +132,12 @@ pub async fn verify_user_application( .map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::InternalServerError, ) })?; - let _commit = hasura_transaction.commit().await.map_err(|e| { + hasura_transaction.commit().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, &format!("commit failed: {e:?}"), @@ -136,24 +148,39 @@ pub async fn verify_user_application( Ok(Json(result)) } +/// Request body for changing application status. #[derive(Deserialize, Debug)] pub struct ApplicationChangeStatusBody { + /// The tenant ID tenant_id: String, + /// The election event ID election_event_id: String, + /// Optional area ID area_id: Option, + /// The application ID id: String, + /// The user ID associated with the application user_id: String, - rejection_reason: Option, // Optional for rejection - rejection_message: Option, // Optional for rejection + /// Optional rejection reason + rejection_reason: Option, + /// Optional rejection message + rejection_message: Option, } +/// Response for application status change operation. #[derive(Serialize, Deserialize, Debug)] pub struct ApplicationChangeStatusOutput { + /// Success message message: Option, + /// Error message if any error: Option, } +/// Changes the status of an application. +/// +/// Requires `APPLICATION_WRITE` permission. #[instrument(skip(claims))] +#[allow(clippy::too_many_lines)] #[post("/change-application-status", format = "json", data = "")] pub async fn change_application_status( claims: jwt::JwtClaims, @@ -162,7 +189,7 @@ pub async fn change_application_status( let input = body.into_inner(); info!("Changing application status: {input:?}"); - info!("claims::: {:?}", &claims); + info!("claims::: {claims:?}"); let required_perm: Permissions = Permissions::APPLICATION_WRITE; authorize( @@ -174,7 +201,7 @@ pub async fn change_application_status( .map_err(|e| { ErrorResponse::new( Status::Unauthorized, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::Unauthorized, ) })?; @@ -183,7 +210,7 @@ pub async fn change_application_status( get_hasura_pool().await.get().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("Error obtaining hasura pool: {:?}", e), + &format!("Error obtaining hasura pool: {e:?}"), ErrorCode::InternalServerError, ) })?; @@ -192,7 +219,7 @@ pub async fn change_application_status( hasura_db_client.transaction().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("Error obtaining transaction: {:?}", e), + &format!("Error obtaining transaction: {e:?}"), ErrorCode::GetTransactionFailed, ) })?; @@ -203,7 +230,7 @@ pub async fn change_application_status( get_group_names(&tenant_realm, user_id).await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("Error getting group names: {:#?}", e), + &format!("Error getting group names: {e:#?}"), ErrorCode::InternalServerError, ) })?; @@ -230,7 +257,7 @@ pub async fn change_application_status( .map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("Error rejecting application: {:?}", e), + &format!("Error rejecting application: {e:?}"), ErrorCode::InternalServerError, ) })?; @@ -239,7 +266,7 @@ pub async fn change_application_status( get_keycloak_pool().await.get().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::GetTransactionFailed, ) })?; @@ -247,7 +274,7 @@ pub async fn change_application_status( keycloak_db_client.transaction().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::GetTransactionFailed, ) })?; @@ -263,7 +290,7 @@ pub async fn change_application_status( .map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("Error in check_is_user_verified: {:?}", e), + &format!("Error in check_is_user_verified: {e:?}"), ErrorCode::InternalServerError, ) })?; @@ -292,7 +319,7 @@ pub async fn change_application_status( .map_err(|e| { ErrorResponse::new( Status::InternalServerError, - &format!("Error confirming application: {:?}", e), + &format!("Error confirming application: {e:?}"), ErrorCode::InternalServerError, ) })?; @@ -302,7 +329,7 @@ pub async fn change_application_status( "Invalid request: rejection_reason and rejection_message must either both be present or both absent", ErrorCode::InternalServerError, ))); - }; + } hasura_transaction.commit().await.map_err(|e| { ErrorResponse::new( diff --git a/packages/harvest/src/routes/areas.rs b/packages/harvest/src/routes/areas.rs index 7df2a876318..7dcb145a25f 100644 --- a/packages/harvest/src/routes/areas.rs +++ b/packages/harvest/src/routes/areas.rs @@ -22,26 +22,41 @@ use windmill::postgres::area_contest::insert_area_to_area_contests; use windmill::services::database::get_hasura_pool; use windmill::services::import::import_election_event::upsert_b3_and_elog; +/// Request body for creating or updating an area. #[derive(Serialize, Deserialize, Debug)] pub struct UpsertAreaInput { + /// Optional area ID; if not provided, a new ID will be generated pub id: Option, + /// Area name pub name: String, + /// Optional area description pub description: Option, + /// The election event this area belongs to pub election_event_id: Uuid, + /// The tenant this area belongs to pub tenant_id: Uuid, + /// Optional parent area ID for hierarchical structure pub parent_id: Option, + /// Associated area contest IDs pub area_contest_ids: Vec, + /// Optional annotations for the area pub annotations: Option, + /// Optional labels for the area pub labels: Option, + /// Optional area type pub r#type: Option, + /// Optional early voting policy pub allow_early_voting: Option, } +/// Response containing the ID of the created or updated area. #[derive(Serialize, Deserialize, Debug)] pub struct UpsertAreaOutput { + /// The area ID id: String, } +/// Creates or updates an area in an election event. #[instrument(skip(claims))] #[post("/upsert-area", format = "json", data = "")] pub async fn upsert_area( @@ -79,10 +94,10 @@ pub async fn upsert_area( ) })?; let area = Area { - id: body - .id - .map(|uuid| uuid.to_string()) - .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()), + id: body.id.map_or_else( + || uuid::Uuid::new_v4().to_string(), + |uuid| uuid.to_string(), + ), tenant_id: body.tenant_id.to_string(), election_event_id: election_event_id_str.clone(), labels: body.labels.clone(), diff --git a/packages/harvest/src/routes/ballot_publication.rs b/packages/harvest/src/routes/ballot_publication.rs index 31b404b6251..fb7168d95cf 100644 --- a/packages/harvest/src/routes/ballot_publication.rs +++ b/packages/harvest/src/routes/ballot_publication.rs @@ -27,17 +27,23 @@ use windmill::{ }, }; +/// Request body for generating a ballot publication. #[derive(Serialize, Deserialize, Debug)] pub struct GenerateBallotPublicationInput { + /// The election event ID election_event_id: String, + /// Optional election ID to filter specific election election_id: Option, } +/// Response containing the generated ballot publication ID. #[derive(Serialize, Deserialize, Debug)] pub struct GenerateBallotPublicationOutput { + /// The ballot publication ID ballot_publication_id: String, } +/// Generates a new ballot publication for an election event/election. #[instrument(skip(claims))] #[post("/generate-ballot-publication", format = "json", data = "")] pub async fn generate_ballot_publication( @@ -62,12 +68,12 @@ pub async fn generate_ballot_publication( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let election_event = get_election_event_by_id( &hasura_transaction, @@ -75,7 +81,7 @@ pub async fn generate_ballot_publication( &input.election_event_id, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; if let Some(election_event_presentation) = election_event.presentation { info!( @@ -90,13 +96,13 @@ pub async fn generate_ballot_publication( if deserialize_value::( election_event_presentation, ) - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))? + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))? .locked_down == Some(LockedDown::LOCKED_DOWN) { return Err(( Status::Forbidden, - format!("Election event is locked down"), + "Election event is locked down".to_string(), )); } } @@ -109,9 +115,9 @@ pub async fn generate_ballot_publication( user_id.clone(), ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - let _commit = hasura_transaction.commit().await.map_err(|err| { + hasura_transaction.commit().await.map_err(|err| { (Status::InternalServerError, format!("Commit failed: {err}")) })?; @@ -120,17 +126,23 @@ pub async fn generate_ballot_publication( })) } +/// Request body for publishing a ballot. #[derive(Serialize, Deserialize, Debug)] pub struct PublishBallotInput { + /// The election event ID election_event_id: String, + /// The ballot publication ID to publish ballot_publication_id: String, } +/// Response containing the published ballot publication ID. #[derive(Serialize, Deserialize, Debug)] pub struct PublishBallotOutput { + /// The published ballot publication ID ballot_publication_id: String, } +/// Publishes a ballot publication for voting. #[instrument(skip(claims))] #[post("/publish-ballot", format = "json", data = "")] pub async fn publish_ballot( @@ -176,25 +188,36 @@ pub async fn publish_ballot( })) } +/// Request body for retrieving ballot publication changes. #[derive(Serialize, Deserialize, Debug)] pub struct GetBallotPublicationChangesInput { + /// The election event ID election_event_id: String, + /// The ballot publication ID ballot_publication_id: String, + /// Optional limit on number of changes to retrieve limit: Option, } +/// Ballot styles information of ballot publication. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BallotPublicationStyles { + /// The ballot publication ID ballot_publication_id: String, + /// The ballot styles as JSON ballot_styles: Value, } +/// Ballot publication changes output. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetBallotPublicationChangesOutput { + /// Current ballot publication styles current: BallotPublicationStyles, + /// Previous ballot publication styles if any previous: Option, } +/// Retrieves changes between ballot publication versions. #[instrument(skip(claims))] #[post("/get-ballot-publication-changes", format = "json", data = "")] pub async fn get_ballot_publication_changes( @@ -214,12 +237,12 @@ pub async fn get_ballot_publication_changes( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let diff = get_ballot_publication_diff( &hasura_transaction, @@ -229,7 +252,7 @@ pub async fn get_ballot_publication_changes( input.limit, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(diff)) } diff --git a/packages/harvest/src/routes/ballot_publication_prepare_preview.rs b/packages/harvest/src/routes/ballot_publication_prepare_preview.rs index 3c1152ba605..953143f6d8c 100644 --- a/packages/harvest/src/routes/ballot_publication_prepare_preview.rs +++ b/packages/harvest/src/routes/ballot_publication_prepare_preview.rs @@ -13,22 +13,30 @@ use serde::{Deserialize, Serialize}; use tracing::{info, instrument}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::types::tasks::ETasksExecution; +/// Request body for preparing ballot publication preview. #[derive(Serialize, Deserialize, Debug)] pub struct PreparePublPreviewInput { + /// The election event ID election_event_id: String, + /// The ballot publication ID ballot_publication_id: String, } +/// Response for ballot publication preview preparation. #[derive(Serialize, Deserialize, Debug)] pub struct PreparePublPreviewOutput { + /// Optional error message error_msg: Option, + /// The generated document ID document_id: String, + /// Task execution information task_execution: Option, } +/// Prepares a ballot publication preview for review. #[instrument(skip(claims))] #[post( "/prepare-ballot-publication-preview", @@ -94,7 +102,7 @@ pub async fn prepare_ballot_publication_preview( Err(err) => { return Ok(Json(PreparePublPreviewOutput { error_msg: Some(format!( - "Error sending prepare_publication_preview task: ${err}" + "Error sending prepare_publication_preview task: {err}" )), document_id, task_execution: Some(task_execution), diff --git a/packages/harvest/src/routes/create_ballot_receipt.rs b/packages/harvest/src/routes/create_ballot_receipt.rs index 8d066159a31..74059ad4fb8 100644 --- a/packages/harvest/src/routes/create_ballot_receipt.rs +++ b/packages/harvest/src/routes/create_ballot_receipt.rs @@ -17,24 +17,37 @@ use windmill::services::celery_app::get_celery_app; use windmill::services::tasks_execution::post; use windmill::types::tasks::ETasksExecution; +/// Request body for creating a ballot receipt. #[derive(Serialize, Deserialize, Debug)] pub struct CreateBallotReceiptInput { + /// The ballot ID pub ballot_id: String, + /// URL for ballot tracking pub ballot_tracker_url: String, + /// The election event ID pub election_event_id: String, + /// The election ID pub election_id: String, + /// Optional timezone for receipt pub time_zone: Option, + /// Optional date format for receipt pub date_format: Option, } +/// Response for ballot receipt creation. #[derive(Serialize, Deserialize, Debug)] pub struct CreateBallotReceiptOutput { + /// The receipt ID pub id: String, + /// The associated ballot ID pub ballot_id: String, + /// The status of the receipt pub status: String, + /// Task execution information pub task_execution: TasksExecution, } +/// Creates a ballot receipt for a voter. #[instrument(skip_all)] #[post("/create-ballot-receipt", format = "json", data = "")] pub async fn create_ballot_receipt( @@ -112,6 +125,6 @@ pub async fn create_ballot_receipt( id: document_id, ballot_id: input.ballot_id, status: "pending".to_string(), - task_execution: task_execution, + task_execution, })) } diff --git a/packages/harvest/src/routes/custom_urls.rs b/packages/harvest/src/routes/custom_urls.rs index ce07d50cca0..213d4a3bc57 100644 --- a/packages/harvest/src/routes/custom_urls.rs +++ b/packages/harvest/src/routes/custom_urls.rs @@ -18,33 +18,49 @@ use windmill::services::custom_url::{ }; use windmill::services::database::get_hasura_pool; +/// Request body for updating custom URL. #[derive(Serialize, Deserialize, Debug)] pub struct UpdateCustomUrlInput { + /// The origin domain pub origin: String, + /// The redirect target URL pub redirect_to: String, + /// The DNS prefix for custom domain pub dns_prefix: String, + /// The election ID pub election_id: String, + /// Authentication key (login/enrollment/saml) pub key: String, } +/// Request body for retrieving custom URL. #[derive(Serialize, Deserialize, Debug)] pub struct GetCustomUrlInput { + /// The redirect target URL pub redirect_to: String, } +/// Response for custom URL retrieval. #[derive(Serialize)] struct GetCustomUrlOutput { + /// Whether the operation was successful success: bool, + /// Status message message: String, + /// The origin domain origin: String, } +/// Response for custom URL update. #[derive(Serialize)] struct UpdateCustomUrlOutput { + /// Whether the operation was successful success: bool, + /// Status message message: String, } +/// Updates a custom URL configuration. #[instrument(skip(claims))] #[post("/set-custom-url", format = "json", data = "")] pub async fn update_custom_url( @@ -67,12 +83,12 @@ pub async fn update_custom_url( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let election_event = get_election_event_by_id( &hasura_transaction, @@ -80,7 +96,7 @@ pub async fn update_custom_url( &body.election_id, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let prev_custom_urls = if let Some(presentation) = &election_event.presentation { @@ -104,16 +120,16 @@ pub async fn update_custom_url( } } else { PreviousCustomUrls { - login: "".to_owned(), - enrollment: "".to_owned(), - saml: "".to_owned(), + login: String::new(), + enrollment: String::new(), + saml: String::new(), } } } else { PreviousCustomUrls { - login: "".to_owned(), - enrollment: "".to_owned(), - saml: "".to_owned(), + login: String::new(), + enrollment: String::new(), + saml: String::new(), } }; @@ -128,15 +144,14 @@ pub async fn update_custom_url( { Ok(message) => { info!("Custom URL successfully updated"); - let success_message = format!("Success updating custom URL"); + let success_message = "Success updating custom URL".to_string(); Ok(Json(UpdateCustomUrlOutput { success: true, message: success_message, })) } Err(error) => { - let error_message = - format!("Error updating custom URL: {:?}", error); + let error_message = format!("Error updating custom URL: {error:?}"); error!("{}", error_message); Ok(Json(UpdateCustomUrlOutput { @@ -147,6 +162,7 @@ pub async fn update_custom_url( } } +/// Retrieves a custom URL configuration. #[instrument(skip(claims))] #[post("/get-custom-url", format = "json", data = "")] pub async fn get_custom_url( @@ -171,7 +187,7 @@ pub async fn get_custom_url( Some(r) => { let origin = r .targets - .get(0) + .first() .map(|target| target.constraint.value.clone()); match origin { @@ -183,14 +199,14 @@ pub async fn get_custom_url( None => Ok(Json(GetCustomUrlOutput { success: false, message: "Error extracting page rule".to_string(), - origin: "".to_string(), + origin: String::new(), })), } } None => Ok(Json(GetCustomUrlOutput { success: false, message: "No matching page rule found".to_string(), - origin: "".to_string(), + origin: String::new(), })), } } diff --git a/packages/harvest/src/routes/delete_certificate_authority.rs b/packages/harvest/src/routes/delete_certificate_authority.rs index f1f191f4612..18479895b4e 100644 --- a/packages/harvest/src/routes/delete_certificate_authority.rs +++ b/packages/harvest/src/routes/delete_certificate_authority.rs @@ -16,17 +16,23 @@ use windmill::postgres::certificate_authority::delete_certificate_authority; use windmill::postgres::election_event::get_election_event_by_id; use windmill::services::database::get_hasura_pool; +/// Request body for deleting a certificate authority. #[derive(Serialize, Deserialize, Debug)] pub struct DeleteCertificateAuthorityInput { + /// The certificate authority ID id: uuid::Uuid, + /// The election event ID election_event_id: uuid::Uuid, } +/// Response for certificate authority deletion. #[derive(Serialize, Deserialize, Debug)] pub struct DeleteCertificateAuthorityOutput { + /// Whether the deletion was successful deleted: bool, } +/// Deletes a certificate authority. #[instrument(skip(claims, input))] #[post("/delete-certificate-authority", format = "json", data = "")] pub async fn delete_certificate_authority_route( diff --git a/packages/harvest/src/routes/delete_election_event.rs b/packages/harvest/src/routes/delete_election_event.rs index 0a466aa52f8..9c2af1ac55f 100644 --- a/packages/harvest/src/routes/delete_election_event.rs +++ b/packages/harvest/src/routes/delete_election_event.rs @@ -14,23 +14,30 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use windmill::postgres::tenant; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::services::tasks_execution::{update_complete, update_fail}; use windmill::tasks::delete_election_event; use windmill::types::tasks::ETasksExecution; +/// Response for election event deletion. #[derive(Serialize, Deserialize, Debug)] pub struct DeleteElectionEventOutput { + /// The election event ID id: String, + /// Task execution information task_execution: TasksExecution, + /// Optional error message error_msg: Option, } +/// Request body for deleting an election event. #[derive(Serialize, Deserialize, Debug)] pub struct DeleteElectionEventInput { + /// The election event ID to delete election_event_id: String, } +/// Deletes an election event and all associated data. #[instrument(skip(claims))] #[post("/delete-election-event", format = "json", data = "")] pub async fn delete_election_event_f( @@ -70,7 +77,7 @@ pub async fn delete_election_event_f( ) .await; return Err(error); - }; + } let celery_app = get_celery_app().await; diff --git a/packages/harvest/src/routes/election_dates.rs b/packages/harvest/src/routes/election_dates.rs index 02d8b43ab92..ac05a6861ef 100644 --- a/packages/harvest/src/routes/election_dates.rs +++ b/packages/harvest/src/routes/election_dates.rs @@ -16,19 +16,27 @@ use tracing::instrument; use windmill::services::database::get_hasura_pool; use windmill::services::{election_dates, election_event_dates}; +/// Request body for managing election dates. #[derive(Deserialize, Debug)] pub struct ManageElectionDatesBody { + /// The election event ID election_event_id: String, + /// Optional election ID within the event election_id: Option, + /// Optional scheduled date scheduled_date: Option, + /// Event processor type event_processor: EventProcessors, } +/// Response for managing election dates. #[derive(Serialize, Deserialize, Debug)] pub struct ManageElectionDatesResponse { + /// Optional error message error_msg: Option, } +/// Manages election dates and scheduled events. #[instrument(skip(claims))] #[post("/manage-election-dates", format = "json", data = "")] pub async fn manage_election_dates( @@ -89,7 +97,7 @@ pub async fn manage_election_dates( ) .await { - Ok(_) => (), + Ok(()) => {} Err(err) => { return Ok(Json(ManageElectionDatesResponse { error_msg: Some(err.to_string()), @@ -116,7 +124,7 @@ pub async fn manage_election_dates( } } - let _commit = hasura_transaction.commit().await.map_err(|e| { + hasura_transaction.commit().await.map_err(|e| { ErrorResponse::new( Status::InternalServerError, &format!("commit failed: {e:?}"), diff --git a/packages/harvest/src/routes/election_event_stats.rs b/packages/harvest/src/routes/election_event_stats.rs index fcf1d698531..b8030a87e74 100644 --- a/packages/harvest/src/routes/election_event_stats.rs +++ b/packages/harvest/src/routes/election_event_stats.rs @@ -23,24 +23,37 @@ use windmill::services::election_event_statistics::{ }; use windmill::services::users::count_keycloak_enabled_users; +/// Request body for fetching election event statistics. #[derive(Serialize, Deserialize, Debug)] pub struct ElectionEventStatsInput { + /// The election event ID election_event_id: String, + /// Start date for vote statistics start_date: String, + /// End date for vote statistics end_date: String, + /// User's timezone for date calculations user_timezone: String, } +/// Response containing election event statistics. #[derive(Serialize, Deserialize, Debug)] pub struct ElectionEventStatsOutput { + /// Total number of eligible voters total_eligible_voters: i64, + /// Total number of distinct voters who cast votes total_distinct_voters: i64, + /// Total number of areas in the election event total_areas: i64, + /// Total number of elections total_elections: i64, + /// Number of votes cast per day votes_per_day: Vec, } +/// Retrieves comprehensive statistics for an election event. #[instrument(skip(claims))] +#[allow(clippy::too_many_lines)] #[post("/election-event/stats", format = "json", data = "")] pub async fn get_election_event_stats( body: Json, @@ -86,8 +99,8 @@ pub async fn get_election_event_stats( let total_distinct_voters: i64 = get_count_distinct_voters( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), ) .await .map_err(|err| { @@ -98,8 +111,8 @@ pub async fn get_election_event_stats( })?; let total_elections: i64 = get_count_elections( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), ) .await .map_err(|err| { @@ -111,8 +124,8 @@ pub async fn get_election_event_stats( let total_areas: i64 = get_count_areas( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), ) .await .map_err(|err| { @@ -124,12 +137,12 @@ pub async fn get_election_event_stats( let votes_per_day: Vec = get_count_votes_per_day( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), - &input.start_date.as_str(), - &input.end_date.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), + input.start_date.as_str(), + input.end_date.as_str(), None, - &input.user_timezone.as_str(), + input.user_timezone.as_str(), ) .await .map_err(|err| { @@ -145,24 +158,31 @@ pub async fn get_election_event_stats( let total_eligible_voters: i64 = count_keycloak_enabled_users(&keycloak_transaction, &realm_name) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(ElectionEventStatsOutput { + total_eligible_voters, total_distinct_voters, total_areas, - total_eligible_voters: total_eligible_voters.into(), - total_elections: total_elections.into(), + total_elections, votes_per_day, })) } +/// Request body for retrieving top votes by IP. #[derive(Deserialize, Debug)] pub struct GetTopCastVotesByIp { + /// The election event ID election_event_id: String, + /// Optional limit on number of results limit: Option, + /// Optional offset for pagination offset: Option, + /// Optional IP address to filter by ip: Option, + /// Optional country code to filter by country: Option, + /// Optional election ID within the event election_id: Option, } @@ -222,7 +242,7 @@ pub async fn get_election_event_top_votes_by_ip( items: cast_votes_by_ip, total: TotalAggregate { aggregate: Aggregate { - count: count as i64, + count: i64::from(count), }, }, })) diff --git a/packages/harvest/src/routes/election_stats.rs b/packages/harvest/src/routes/election_stats.rs index 4bb55e0fe53..1e9658094f1 100644 --- a/packages/harvest/src/routes/election_stats.rs +++ b/packages/harvest/src/routes/election_stats.rs @@ -18,22 +18,33 @@ use windmill::services::database::get_hasura_pool; use windmill::services::election_statistics::get_count_areas; use windmill::services::election_statistics::get_count_distinct_voters; +/// Request body for [`get_election_stats`]. #[derive(Serialize, Deserialize, Debug)] pub struct ElectionStatsInput { + /// Election event UUID. election_event_id: String, + /// Election UUID. election_id: String, + /// Inclusive start of the date range (ISO-8601 date or datetime string). start_date: String, + /// Inclusive end of the date range (ISO-8601 date or datetime string). end_date: String, + /// User's timezone for date calculations. user_timezone: String, } +/// Aggregated statistics for a single election. #[derive(Serialize, Deserialize, Debug)] pub struct ElectionStatsOutput { + /// Total number of distinct voters. total_distinct_voters: i64, + /// Number of areas in the election. total_areas: i64, + /// Vote counts grouped by calendar day. votes_per_day: Vec, } +/// Returns voter counts, area counts, and daily vote totals for one election. #[instrument(skip(claims))] #[post("/election/stats", format = "json", data = "")] pub async fn get_election_stats( @@ -66,9 +77,9 @@ pub async fn get_election_stats( let total_distinct_voters: i64 = get_count_distinct_voters( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), - &input.election_id.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), + input.election_id.as_str(), ) .await .map_err(|err| { @@ -79,9 +90,9 @@ pub async fn get_election_stats( })?; let total_areas: i64 = get_count_areas( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), - &input.election_id.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), + input.election_id.as_str(), ) .await .map_err(|err| { @@ -93,12 +104,12 @@ pub async fn get_election_stats( let votes_per_day: Vec = get_count_votes_per_day( &hasura_transaction, - &tenant_id.as_str(), - &input.election_event_id.as_str(), - &input.start_date.as_str(), - &input.end_date.as_str(), + tenant_id.as_str(), + input.election_event_id.as_str(), + input.start_date.as_str(), + input.end_date.as_str(), Some(input.election_id), - &input.user_timezone.as_str(), + input.user_timezone.as_str(), ) .await .map_err(|err| { diff --git a/packages/harvest/src/routes/elections.rs b/packages/harvest/src/routes/elections.rs index d5c7f6ab6b3..1c77d89ca43 100644 --- a/packages/harvest/src/routes/elections.rs +++ b/packages/harvest/src/routes/elections.rs @@ -19,18 +19,26 @@ use windmill::services::database::get_hasura_pool; use windmill::services::import::import_election_event::upsert_b3_and_elog; #[derive(Serialize, Deserialize, Debug)] +/// Request body for creating an election. pub struct CreateElectionInput { + /// The election event ID. election_event_id: String, + /// The external ID of the election. external_id: String, + /// The presentation of the election. presentation: ElectionPresentation, + /// The description of the election. description: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the ID of the created election. pub struct CreateElectionOutput { + /// The ID of the created election. id: String, } +/// Creates an election. #[instrument(skip(claims))] #[post("/create-election", format = "json", data = "")] pub async fn create_election( @@ -48,12 +56,12 @@ pub async fn create_election( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let election = election::create_election( &hasura_transaction, @@ -64,7 +72,7 @@ pub async fn create_election( &body.external_id, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; upsert_b3_and_elog( &hasura_transaction, @@ -74,13 +82,13 @@ pub async fn create_election( false, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; hasura_transaction .commit() .await .with_context(|| "error comitting transaction") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(CreateElectionOutput { id: election.id })) } diff --git a/packages/harvest/src/routes/electoral_log.rs b/packages/harvest/src/routes/electoral_log.rs index 32a52d88b1f..9c77a28caaa 100644 --- a/packages/harvest/src/routes/electoral_log.rs +++ b/packages/harvest/src/routes/electoral_log.rs @@ -16,6 +16,7 @@ use windmill::services::electoral_log::{ }; use windmill::types::resources::DataList; +/// Returns a list of electoral log entries. #[instrument] #[post("/immudb/electoral-log", format = "json", data = "")] pub async fn list_electoral_log( @@ -31,7 +32,7 @@ pub async fn list_electoral_log( )?; let ret_val = get_logs(input) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(ret_val)) } diff --git a/packages/harvest/src/routes/error_catchers.rs b/packages/harvest/src/routes/error_catchers.rs index 77e7e089e29..b11a530c3b5 100644 --- a/packages/harvest/src/routes/error_catchers.rs +++ b/packages/harvest/src/routes/error_catchers.rs @@ -8,10 +8,13 @@ use serde::{Deserialize, Serialize}; use tracing::instrument; #[derive(Serialize, Deserialize, Debug)] +/// Response containing the error message. pub struct ErrorResponse { + /// The error message. message: String, } +/// Returns an internal server error response. #[instrument] #[catch(500)] pub fn internal_error() -> Json { @@ -20,6 +23,7 @@ pub fn internal_error() -> Json { }) } +/// Returns a not found response. #[instrument(skip_all)] #[catch(404)] pub fn not_found(_req: &Request) -> Json { @@ -28,6 +32,7 @@ pub fn not_found(_req: &Request) -> Json { }) } +/// Returns a default error response. #[instrument(skip_all)] #[catch(default)] pub fn default(_status: Status, _req: &Request) -> Json { diff --git a/packages/harvest/src/routes/export_application.rs b/packages/harvest/src/routes/export_application.rs index 7be65d5842f..0d2ad590431 100644 --- a/packages/harvest/src/routes/export_application.rs +++ b/packages/harvest/src/routes/export_application.rs @@ -12,22 +12,32 @@ use serde::{Deserialize, Serialize}; use tracing::instrument; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_complete, update_fail}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting an application. +#[allow(clippy::struct_field_names)] // enable same postfix for all fields pub struct ExportApplicationBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: String, + /// The election ID. election_id: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportApplicationOutput { + /// The document ID. document_id: String, + /// The error message. error_msg: Option, + /// The task execution. task_execution: TasksExecution, } +/// Genarate application export file. #[instrument(skip(claims))] #[post("/export-application", format = "json", data = "")] pub async fn export_application_route( diff --git a/packages/harvest/src/routes/export_ballot_publication.rs b/packages/harvest/src/routes/export_ballot_publication.rs index a99194d3225..2665967bff1 100644 --- a/packages/harvest/src/routes/export_ballot_publication.rs +++ b/packages/harvest/src/routes/export_ballot_publication.rs @@ -14,25 +14,35 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_fail}; use windmill::tasks::export_ballot_publication::export_ballot_publication; use windmill::tasks::export_election_event::{self, ExportOptions}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for exporting a ballot publication. pub struct ExportBallotPublicationInput { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: String, + /// The election ID. election_id: Option, + /// The ballot publication ID. ballot_publication_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportBallotPublicationOutput { + /// The generated document ID. document_id: String, + /// Task execution record. task_execution: TasksExecution, } +/// Genarate ballot publication export file. #[instrument(skip(claims))] #[post("/export-ballot-publication", format = "json", data = "")] pub async fn export_ballot_publication_route( @@ -71,11 +81,11 @@ pub async fn export_ballot_publication_route( ) { update_fail( &task_execution, - &format!("Failed to authorize executing the task: {:?}", error), + &format!("Failed to authorize executing the task: {error:?}"), ) .await; return Err(error); - }; + } let document_id = Uuid::new_v4().to_string(); let celery_app = get_celery_app().await; diff --git a/packages/harvest/src/routes/export_election_event.rs b/packages/harvest/src/routes/export_election_event.rs index e0232601cc6..24e4e970cfc 100644 --- a/packages/harvest/src/routes/export_election_event.rs +++ b/packages/harvest/src/routes/export_election_event.rs @@ -14,23 +14,31 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::{password, tasks_execution::*}; +use windmill::services::{password, tasks_execution::post}; use windmill::tasks::export_election_event::{self, ExportOptions}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting an election event. pub struct ExportElectionEventInput { + /// The election event ID. election_event_id: String, + /// The export configurations. export_configurations: ExportOptions, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportElectionEventOutput { + /// The generated document ID. document_id: String, + /// Password for the exported document. password: Option, + /// Task execution record. task_execution: TasksExecution, } +/// Genarate election event export file. #[instrument(skip(claims))] #[post("/export-election-event", format = "json", data = "")] pub async fn export_election_event_route( @@ -84,7 +92,7 @@ pub async fn export_election_event_route( } else { None }; - export_config.password = password.clone(); + export_config.password.clone_from(&password); let celery_task = celery_app .send_task(export_election_event::export_election_event::new( diff --git a/packages/harvest/src/routes/export_election_event_logs.rs b/packages/harvest/src/routes/export_election_event_logs.rs index f3298c0e65b..21041552edf 100644 --- a/packages/harvest/src/routes/export_election_event_logs.rs +++ b/packages/harvest/src/routes/export_election_event_logs.rs @@ -15,21 +15,28 @@ use tracing::instrument; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; use windmill::services::reports::activity_log::ReportFormat; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting election event logs. pub struct ExportElectionEventInput { + /// The election event ID. election_event_id: String, + /// The format of the report. format: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportElectionEventOutput { + /// The generated document ID. document_id: String, + /// Task execution record. task_execution: TasksExecution, } +/// Genarate election event logs export file. #[instrument(skip(claims))] #[post("/export-election-event-logs", format = "json", data = "")] pub async fn export_election_event_logs_route( diff --git a/packages/harvest/src/routes/export_tally_results.rs b/packages/harvest/src/routes/export_tally_results.rs index ee75c829de0..659c7ce30b6 100644 --- a/packages/harvest/src/routes/export_tally_results.rs +++ b/packages/harvest/src/routes/export_tally_results.rs @@ -14,24 +14,32 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::tasks::export_ballot_publication::export_ballot_publication; use windmill::tasks::export_election_event::{self, ExportOptions}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting tally results. pub struct ExportTallyResultsInput { + /// The election event ID. election_event_id: String, + /// The tally session ID. tally_session_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportTallyResultsOutput { + /// The generated document ID. document_id: String, + /// Task execution record. task_execution: TasksExecution, + /// Error message if any. error_msg: Option, } +/// Genarate tally results export file. #[instrument(skip(claims))] #[post("/export-tally-results", format = "json", data = "")] pub async fn export_tally_results_route( diff --git a/packages/harvest/src/routes/export_tasks_execution.rs b/packages/harvest/src/routes/export_tasks_execution.rs index b45eeb167be..86a3a1b1349 100644 --- a/packages/harvest/src/routes/export_tasks_execution.rs +++ b/packages/harvest/src/routes/export_tasks_execution.rs @@ -14,17 +14,24 @@ use windmill::services::celery_app::get_celery_app; use windmill::tasks::export_tasks_execution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting tasks execution. pub struct ExportTasksExecutionBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportTasksExecutionOutput { + /// The generated document ID. document_id: String, + /// Error message if any. error_msg: Option, } +/// Genarate tasks execution export file. #[instrument(skip(claims))] #[post("/export-tasks-execution", format = "json", data = "")] pub async fn export_tasks_execution_route( diff --git a/packages/harvest/src/routes/export_template.rs b/packages/harvest/src/routes/export_template.rs index f153c2b6950..87ce9486556 100644 --- a/packages/harvest/src/routes/export_template.rs +++ b/packages/harvest/src/routes/export_template.rs @@ -12,21 +12,29 @@ use serde::{Deserialize, Serialize}; use tracing::instrument; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_fail}; use windmill::tasks::export_templates; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting a template. pub struct ExportTemplateBody { + /// The tenant ID. tenant_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportTemplateOutput { + /// The generated document ID. document_id: String, + /// Error message if any. error_msg: Option, + /// Task execution record. task_execution: TasksExecution, } + +/// Genarate file for template. #[instrument(skip(claims))] #[post("/export-template", format = "json", data = "")] pub async fn export_template( @@ -68,7 +76,7 @@ pub async fn export_template( ) .await; return Err(error); - }; + } let document_id = Uuid::new_v4().to_string(); diff --git a/packages/harvest/src/routes/export_tenant_config.rs b/packages/harvest/src/routes/export_tenant_config.rs index f99503055c9..7b7bc5aa3c9 100644 --- a/packages/harvest/src/routes/export_tenant_config.rs +++ b/packages/harvest/src/routes/export_tenant_config.rs @@ -14,22 +14,29 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::tasks::export_tenant_config::{self}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting tenant config. pub struct ExportTenantConfigInput { + /// The tenant ID. tenant_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the exported document. pub struct ExportTenantConfigOutput { + /// The generated document ID. document_id: String, + /// Error message if any. error_msg: Option, + /// Task execution record. task_execution: TasksExecution, } +/// Genarate tenant config export file. #[instrument(skip(claims))] #[post("/export-tenant-config", format = "json", data = "")] pub async fn export_tenant_config_route( diff --git a/packages/harvest/src/routes/fetch_document.rs b/packages/harvest/src/routes/fetch_document.rs index 92c987bab59..8509fea635a 100644 --- a/packages/harvest/src/routes/fetch_document.rs +++ b/packages/harvest/src/routes/fetch_document.rs @@ -14,16 +14,22 @@ use tracing::instrument; use windmill::services::{database::get_hasura_pool, documents}; #[derive(Deserialize, Debug)] +/// Request body for fetching a document URL. pub struct GetDocumentUrlBody { + /// The election event ID. election_event_id: Option, + /// The document ID. document_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the document URL. pub struct GetDocumentUrlResponse { + /// The document URL. url: String, } +/// Fetch a document URL. #[instrument(skip(claims))] #[post("/fetch-document", format = "json", data = "")] pub async fn fetch_document( @@ -63,7 +69,7 @@ pub async fn fetch_document( &input.document_id, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))? + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))? .ok_or_else(|| (Status::NotFound, "Document not found".to_string()))?; hasura_transaction.commit().await.map_err(|err| { diff --git a/packages/harvest/src/routes/generate_preview_url.rs b/packages/harvest/src/routes/generate_preview_url.rs index d8296f3a7b0..b0f543cf619 100644 --- a/packages/harvest/src/routes/generate_preview_url.rs +++ b/packages/harvest/src/routes/generate_preview_url.rs @@ -19,16 +19,22 @@ use windmill::services::tasks_execution::*; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for generating a preview URL. pub struct GeneratePreviewUrlInput { + /// The tenant ID. tenant_id: String, + /// The ballot style document ID. document_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the preview URL. pub struct GeneratePreviewUrlOutput { + /// The preview URL. preview_url: String, } +/// Generate a preview URL by ballot style document ID. #[instrument(skip(claims))] #[post("/generate-preview-url", format = "json", data = "")] pub async fn generate_preview_url( diff --git a/packages/harvest/src/routes/google_meet.rs b/packages/harvest/src/routes/google_meet.rs index b1a91264087..8934b9e145a 100644 --- a/packages/harvest/src/routes/google_meet.rs +++ b/packages/harvest/src/routes/google_meet.rs @@ -17,10 +17,13 @@ use windmill::services::google_meet::{ }; #[derive(Serialize, Deserialize, Debug)] +/// Response containing the Google Meet link. pub struct GenerateGoogleMeetOutput { - pub meet_link: Option, + /// The Google Meet link. + meet_link: Option, } +/// Generate a Google Meet link. #[instrument(skip(claims))] #[post("/generate-google-meeting", format = "json", data = "")] pub async fn generate_google_meeting( diff --git a/packages/harvest/src/routes/immudb_log_audit.rs b/packages/harvest/src/routes/immudb_log_audit.rs index 0bcbaaafd52..faba707872f 100644 --- a/packages/harvest/src/routes/immudb_log_audit.rs +++ b/packages/harvest/src/routes/immudb_log_audit.rs @@ -22,6 +22,7 @@ use tracing::instrument; use windmill::services::database::PgConfig; #[instrument(err)] +/// Get an immudb client. pub async fn get_immudb_client() -> Result { let username = env::var("IMMUDB_USER").context("IMMUDB_USER must be set")?; @@ -36,27 +37,37 @@ pub async fn get_immudb_client() -> Result { Ok(client) } -// Helper function to create a NamedParam -pub fn create_named_param(name: String, value: Value) -> NamedParam { +/// Helper function to create a `NamedParam` +pub const fn create_named_param(name: String, value: Value) -> NamedParam { NamedParam { name, value: Some(SqlValue { value: Some(value) }), } } -// Enumeration for the valid fields in the immudb table +/// Enumeration for the valid fields in the immudb table #[derive(Debug, Deserialize, Hash, PartialEq, Eq, EnumString, Display)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] +/// Enumeration for the valid fields in the immudb table enum OrderField { + /// The ID of the audit entry. Id, + /// The type of the audit entry. AuditType, + /// The class of the audit entry. Class, + /// The command of the audit entry. Command, + /// The database name of the audit entry. Dbname, + /// The server timestamp of the audit entry. ServerTimestamp, + /// The session ID of the audit entry. SessionId, + /// The statement of the audit entry. Statement, + /// The user of the audit entry. User, } @@ -65,26 +76,37 @@ enum OrderField { )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] +/// Enumeration for the valid audit tables in the immudb database enum AuditTable { + /// The Hasura audit table. #[default] PgauditHasura, + /// The Keycloak audit table. PgauditKeycloak, } #[derive(Deserialize, Debug)] +/// Request body for getting a list of audit entries. pub struct GetPgauditBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: String, + /// The limit of the audit entries. limit: Option, + /// The offset of the audit entries. offset: Option, + /// Filters for the audit entries. filter: Option>, + /// Order by for the audit entries. order_by: Option>, + /// Audit table to use. #[serde(default)] audit_table: AuditTable, } impl GetPgauditBody { - // Returns the SQL clauses related to the request along with the parameters + /// Returns the SQL clauses related to the request along with the parameters #[instrument(ret)] fn as_sql(&self, to_count: bool) -> Result<(String, Vec)> { let mut clauses = Vec::new(); @@ -99,7 +121,7 @@ impl GetPgauditBody { match field { OrderField::Id => { let int_value: i64 = value.parse()?; - where_clauses.push(format!("id = @{}", param_name)); + where_clauses.push(format!("id = @{param_name}")); params.push(create_named_param( param_name, Value::N(int_value), @@ -108,10 +130,10 @@ impl GetPgauditBody { OrderField::ServerTimestamp => {} // Not supported _ => { where_clauses - .push(format!("{field} LIKE @{}", param_name)); + .push(format!("{field} LIKE @{param_name}")); params.push(create_named_param( param_name, - Value::S(value.to_string()), + Value::S(value.clone()), )); } } @@ -123,15 +145,15 @@ impl GetPgauditBody { } // Handle order_by - if !to_count && self.order_by.is_some() { - let order_by_clauses: Vec = self - .order_by - .as_ref() - .unwrap() - .iter() - .map(|(field, direction)| format!("{field} {direction}")) - .collect(); - clauses.push(format!("ORDER BY {}", order_by_clauses.join(", "))); + if !to_count { + if let Some(order_by) = &self.order_by { + let order_by_clauses: Vec = order_by + .iter() + .map(|(field, direction)| format!("{field} {direction}")) + .collect(); + clauses + .push(format!("ORDER BY {}", order_by_clauses.join(", "))); + } } // Handle limit @@ -149,12 +171,16 @@ impl GetPgauditBody { } // Handle offset - if !to_count && self.offset.is_some() { - let offset_param_name = String::from("offset"); - let offset = std::cmp::max(self.offset.unwrap(), 0); - clauses.push(format!("OFFSET @{}", offset_param_name)); - params - .push(create_named_param(offset_param_name, Value::N(offset))); + if !to_count { + if let Some(raw_offset) = self.offset { + let offset_param_name = String::from("offset"); + let offset = std::cmp::max(raw_offset, 0); + clauses.push(format!("OFFSET @{offset_param_name}")); + params.push(create_named_param( + offset_param_name, + Value::N(offset), + )); + } } Ok((clauses.join(" "), params)) @@ -162,15 +188,25 @@ impl GetPgauditBody { } #[derive(Serialize, Deserialize, Debug)] +/// Response containing the audit entry. pub struct PgAuditRow { + /// The ID of the audit entry. id: i64, + /// The type of the audit entry. audit_type: String, + /// The class of the audit entry. class: String, + /// The command of the audit entry. command: String, + /// The database name of the audit entry. dbname: String, + /// The server timestamp of the audit entry. server_timestamp: i64, + /// The session ID of the audit entry. session_id: String, + /// The statement of the audit entry. statement: String, + /// The user of the audit entry. user: String, } @@ -179,47 +215,47 @@ impl TryFrom<&Row> for PgAuditRow { fn try_from(row: &Row) -> Result { let mut id = 0; - let _audit_type = String::from(""); - let mut class = String::from(""); - let mut command = String::from(""); - let mut dbname = String::from(""); + let _audit_type = String::new(); + let mut class = String::new(); + let mut command = String::new(); + let mut dbname = String::new(); let mut server_timestamp: i64 = 0; - let mut session_id = String::from(""); - let mut statement = String::from(""); - let mut user = String::from(""); - let mut audit_type = String::from(""); + let mut session_id = String::new(); + let mut statement = String::new(); + let mut user = String::new(); + let mut audit_type = String::new(); for (column, value) in row.columns.iter().zip(row.values.iter()) { match column.as_str() { c if c.ends_with(".id)") => { - assign_value!(Value::N, value, id) + assign_value!(Value::N, value, id); } c if c.ends_with(".audit_type)") => { - assign_value!(Value::S, value, audit_type) + assign_value!(Value::S, value, audit_type); } c if c.ends_with(".class)") => { - assign_value!(Value::S, value, class) + assign_value!(Value::S, value, class); } c if c.ends_with(".command)") => { - assign_value!(Value::S, value, command) + assign_value!(Value::S, value, command); } c if c.ends_with(".dbname)") => { - assign_value!(Value::S, value, dbname) + assign_value!(Value::S, value, dbname); } c if c.ends_with(".server_timestamp)") => { - assign_value!(Value::Ts, value, server_timestamp) + assign_value!(Value::Ts, value, server_timestamp); } c if c.ends_with(".session_id)") => { - assign_value!(Value::S, value, session_id) + assign_value!(Value::S, value, session_id); } c if c.ends_with(".statement)") => { - assign_value!(Value::S, value, statement) + assign_value!(Value::S, value, statement); } c if c.ends_with(".user)") => { - assign_value!(Value::S, value, user) + assign_value!(Value::S, value, user); } c if c.ends_with(".audit_type)") => { - assign_value!(Value::S, value, audit_type) + assign_value!(Value::S, value, audit_type); } _ => { return Err(anyhow!( @@ -243,6 +279,7 @@ impl TryFrom<&Row> for PgAuditRow { } } +/// Get a list of audit entries. async fn audit_list_service( input: GetPgauditBody, ) -> Result>, Debug> { @@ -254,7 +291,7 @@ async fn audit_list_service( let audit_table = input.audit_table; let sql = format!( - r#" + r" SELECT id, audit_type, @@ -267,7 +304,7 @@ async fn audit_list_service( user FROM {audit_table} {clauses} - "#, + ", ); let sql_query_response = client.sql_query(&sql, params).await?; let items = sql_query_response @@ -277,16 +314,17 @@ async fn audit_list_service( .map(PgAuditRow::try_from) .collect::>>()?; - let sql = format!( - r#" + let count_sql = format!( + r" SELECT COUNT(*) FROM {audit_table} {clauses_to_count} - "#, + ", ); - let sql_query_response = client.sql_query(&sql, count_params).await?; - let mut rows_iter = sql_query_response + let count_sql_query_response = + client.sql_query(&count_sql, count_params).await?; + let mut rows_iter = count_sql_query_response .get_ref() .rows .iter() @@ -298,14 +336,13 @@ async fn audit_list_service( client.close_session().await?; Ok(Json(DataList { - items: items, - total: TotalAggregate { - aggregate: aggregate, - }, + items, + total: TotalAggregate { aggregate }, })) } -#[instrument] +/// Get a list of audit entries endpoint. +#[instrument(skip(claims))] #[post("/immudb/pgaudit-list", format = "json", data = "")] pub async fn list_pgaudit( body: Json, @@ -320,7 +357,7 @@ pub async fn list_pgaudit( )?; let result = audit_list_service(input) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(result) } diff --git a/packages/harvest/src/routes/import_application.rs b/packages/harvest/src/routes/import_application.rs index 6334d45ae5d..9e4e0c54c40 100644 --- a/packages/harvest/src/routes/import_application.rs +++ b/packages/harvest/src/routes/import_application.rs @@ -12,7 +12,7 @@ use sequent_core::types::permissions::Permissions; use serde::{Deserialize, Serialize}; use tracing::{event, info, instrument, Level}; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::types::tasks::ETasksExecution; use windmill::{ services::providers::transactions_provider::provide_hasura_transaction, @@ -20,21 +20,32 @@ use windmill::{ }; #[derive(Serialize, Deserialize, Debug)] +/// Request body for importing applications. pub struct ImportApplicationsInput { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: String, + /// The election ID. election_id: Option, + /// The document ID. document_id: String, + /// The SHA-256 hash of the document. sha256: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing applications. pub struct ImportApplicationsOutput { + /// The error message. error_msg: Option, + /// The document ID. document_id: String, + /// The task execution. task_execution: Option, } +/// Import applications. #[instrument(skip(claims))] #[post("/import-application", format = "json", data = "")] pub async fn import_application_route( diff --git a/packages/harvest/src/routes/import_areas.rs b/packages/harvest/src/routes/import_areas.rs index 7758650246b..be62b537640 100644 --- a/packages/harvest/src/routes/import_areas.rs +++ b/packages/harvest/src/routes/import_areas.rs @@ -16,15 +16,21 @@ use windmill::{ }; #[derive(Serialize, Deserialize, Debug)] +/// Request body for importing areas. pub struct ImportAreasInput { + /// The election event ID. election_event_id: String, + /// The document ID. document_id: String, + /// The SHA-256 hash of the document. sha256: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing areas. pub struct ImportAreasOutput {} +/// Upsert areas. #[instrument(skip(claims))] #[post("/upsert-areas", format = "json", data = "")] pub async fn upsert_areas_route( @@ -54,6 +60,7 @@ pub async fn upsert_areas_route( Ok(Json(ImportAreasOutput {})) } +/// Import areas. #[instrument(skip(claims))] #[post("/import-areas", format = "json", data = "")] pub async fn import_areas_route( diff --git a/packages/harvest/src/routes/import_candidates.rs b/packages/harvest/src/routes/import_candidates.rs index d75f45770d9..842311f49eb 100644 --- a/packages/harvest/src/routes/import_candidates.rs +++ b/packages/harvest/src/routes/import_candidates.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only +#![allow(clippy::large_futures)] use crate::services::authorization::authorize; use anyhow::Result; @@ -11,26 +12,35 @@ use sequent_core::types::hasura::core::TasksExecution; use sequent_core::types::permissions::Permissions; use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::{ tasks::import_candidates::import_candidates_task, types::tasks::ETasksExecution, }; #[derive(Serialize, Deserialize, Debug)] +/// Request body for importing candidates. pub struct ImportCandidatesInput { + /// The election event ID. election_event_id: String, + /// The document ID. document_id: String, + /// The SHA-256 hash of the document. sha256: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing candidates. pub struct ImportCandidatesOutput { + /// The error message. error_msg: Option, + /// The document ID. document_id: String, + /// The task execution. task_execution: TasksExecution, } +/// Import candidates. #[instrument(skip(claims))] #[post("/import-candidates", format = "json", data = "")] pub async fn import_candidates_route( @@ -76,7 +86,7 @@ pub async fn import_candidates_route( ) .await { - Ok(_) => (), + Ok(()) => (), Err(err) => { return Ok(Json(ImportCandidatesOutput { error_msg: Some(err.to_string()), @@ -84,7 +94,7 @@ pub async fn import_candidates_route( task_execution: task_execution.clone(), })); } - }; + } let output = ImportCandidatesOutput { error_msg: None, diff --git a/packages/harvest/src/routes/import_certificate_authority.rs b/packages/harvest/src/routes/import_certificate_authority.rs index 97494365425..3ade0d37ac0 100644 --- a/packages/harvest/src/routes/import_certificate_authority.rs +++ b/packages/harvest/src/routes/import_certificate_authority.rs @@ -23,20 +23,30 @@ use windmill::services::certificate_authority::{ use windmill::services::database::get_hasura_pool; #[derive(Serialize, Deserialize, Debug)] +/// Request body for importing certificate authority. pub struct ImportCertificateAuthorityInput { + /// The election event ID. election_event_id: uuid::Uuid, + /// The PEM content of the certificate authority. pem_content: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing certificate authority. pub struct ImportCertificateAuthorityOutput { + /// The number of inserted certificates. inserted_count: i32, + /// The number of skipped certificates. skipped_count: i32, + /// The errors. errors: Vec, } +/// Import certificate authority. #[instrument(skip(claims, input))] #[post("/import-certificate-authority", format = "json", data = "")] +#[allow(clippy::too_many_lines)] +#[allow(clippy::arithmetic_side_effects)] pub async fn import_certificate_authority( claims: JwtClaims, input: Json, diff --git a/packages/harvest/src/routes/import_templates.rs b/packages/harvest/src/routes/import_templates.rs index 21d27b00312..9e8a6faa91f 100644 --- a/packages/harvest/src/routes/import_templates.rs +++ b/packages/harvest/src/routes/import_templates.rs @@ -18,23 +18,32 @@ use windmill::{ }; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_fail}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for importing templates. pub struct ImportTemplatesInput { + /// The tenant ID. tenant_id: String, + /// The document ID. document_id: String, + /// The SHA-256 hash of the document. sha256: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing templates. pub struct ImportTemplatesOutput { + /// The error message. error_msg: Option, + /// The document ID. document_id: String, + /// The task execution. task_execution: TasksExecution, } +/// Import templates. #[instrument(skip(claims))] #[post("/import-templates", format = "json", data = "")] pub async fn import_templates_route( @@ -74,7 +83,7 @@ pub async fn import_templates_route( ) .await; return Err(error); - }; + } let celery_app = get_celery_app().await; let document_id = body.document_id.clone(); diff --git a/packages/harvest/src/routes/import_tenant_config.rs b/packages/harvest/src/routes/import_tenant_config.rs index 8f348093455..f5339dbcdbc 100644 --- a/packages/harvest/src/routes/import_tenant_config.rs +++ b/packages/harvest/src/routes/import_tenant_config.rs @@ -14,24 +14,34 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::tasks::import_tenant_config::{self, ImportOptions}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for importing tenant config. pub struct ImportTenantConfigInput { + /// The tenant ID. tenant_id: String, + /// The document ID. document_id: String, + /// The import configurations. import_configurations: ImportOptions, + /// The SHA-256 hash of the document. sha256: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing tenant config. pub struct ImportTenantConfigOutput { + /// The message. message: Option, + /// The error message. error: Option, + /// The task execution. task_execution: Option, } +/// Import tenant config. #[instrument(skip(claims))] #[post("/import-tenant-config", format = "json", data = "")] pub async fn import_tenant_config_route( @@ -88,7 +98,7 @@ pub async fn import_tenant_config_route( })?; let output = ImportTenantConfigOutput { - message: Some(format!("Upserted Tenant Config successfully")), + message: Some("Upserted Tenant Config successfully".to_string()), error: None, task_execution: Some(task_execution.clone()), }; diff --git a/packages/harvest/src/routes/insert_cast_vote.rs b/packages/harvest/src/routes/insert_cast_vote.rs index bb68e433745..02ab210d0dc 100644 --- a/packages/harvest/src/routes/insert_cast_vote.rs +++ b/packages/harvest/src/routes/insert_cast_vote.rs @@ -28,6 +28,8 @@ use windmill::services::insert_cast_vote::{ /// returning the error. #[instrument(skip_all)] #[post("/insert-cast-vote", format = "json", data = "")] +#[allow(clippy::too_many_lines)] +#[allow(clippy::large_futures)] pub async fn insert_cast_vote( body: Json, claims: JwtClaims, @@ -45,17 +47,17 @@ pub async fn insert_cast_vote( .map_err(|e| { ErrorResponse::new( Status::Unauthorized, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::Unauthorized, ) })?; info!("insert-cast-vote: starting"); - let insert_result_wrapped = retry_with_exponential_backoff( + let insert_result_wrapped = Box::pin(retry_with_exponential_backoff( // The closure we want to call repeatedly || async { - try_insert_cast_vote( + Box::pin(try_insert_cast_vote( input.clone(), &claims.hasura_claims.tenant_id, &claims.hasura_claims.user_id, @@ -63,18 +65,15 @@ pub async fn insert_cast_vote( voting_channel, &claims.auth_time, &user_info.ip.map(|ip| ip.to_string()), - &user_info - .country_code - .clone() - .map(|country_code| country_code.to_string()), - ) + &user_info.country_code, + )) .await }, // Maximum number of retries: 5, // Initial backoff: Duration::from_millis(100), - ) + )) .await; // Unwrap SkipRetryFailure into a normal Result/Error @@ -129,11 +128,6 @@ pub async fn insert_cast_vote( ErrorCode::CheckStatusFailed.to_string().as_str(), ErrorCode::CheckStatusFailed, ), - CastVoteError::CheckStatusInternalFailed(_) => ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ), CastVoteError::CheckPreviousVotesFailed(msg) => { ErrorResponse::new( Status::BadRequest, @@ -160,17 +154,11 @@ pub async fn insert_cast_vote( ErrorCode::InsertFailedExceedsAllowedRevotes.to_string().as_str(), ErrorCode::InsertFailedExceedsAllowedRevotes, ), - CastVoteError::InsertFailed(_) => ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ), - CastVoteError::CommitFailed(_) => ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ), - CastVoteError::GetDbClientFailed(_) => ErrorResponse::new( + CastVoteError::CheckStatusInternalFailed(_) | CastVoteError::InsertFailed(_) + | CastVoteError::CommitFailed(_) | CastVoteError::GetDbClientFailed(_) + | CastVoteError::SerializeVoterIdFailed(_) | CastVoteError::SerializeBallotFailed(_) + | CastVoteError::BallotSignFailed(_) | CastVoteError::BallotVoterSignatureFailed(_) + => ErrorResponse::new( Status::InternalServerError, ErrorCode::InternalServerError.to_string().as_str(), ErrorCode::InternalServerError, @@ -215,44 +203,20 @@ pub async fn insert_cast_vote( ErrorCode::DeserializeAreaPresentationFailed, ) } - CastVoteError::SerializeVoterIdFailed(_) => { - ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ) - } - CastVoteError::SerializeBallotFailed(_) => { - ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ) - } CastVoteError::PokValidationFailed(_) => { ErrorResponse::new( Status::BadRequest, ErrorCode::PokValidationFailed.to_string().as_str(), ErrorCode::PokValidationFailed, ) - } - CastVoteError::BallotSignFailed(_) => ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ), - CastVoteError::BallotVoterSignatureFailed(_) => ErrorResponse::new( - Status::InternalServerError, - ErrorCode::InternalServerError.to_string().as_str(), - ErrorCode::InternalServerError, - ), + }, CastVoteError::UuidParseFailed(_, _) => { ErrorResponse::new( Status::BadRequest, ErrorCode::UuidParseFailed.to_string().as_str(), ErrorCode::UuidParseFailed, ) - } + }, CastVoteError::UnknownError(_) => ErrorResponse::new( Status::InternalServerError, ErrorCode::UnknownError.to_string().as_str(), diff --git a/packages/harvest/src/routes/insert_election_event.rs b/packages/harvest/src/routes/insert_election_event.rs index 4dbc4134f40..90c882f0e9f 100644 --- a/packages/harvest/src/routes/insert_election_event.rs +++ b/packages/harvest/src/routes/insert_election_event.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only +#![allow(clippy::large_futures)] use crate::services::authorization::authorize; use anyhow::Result; @@ -22,21 +23,25 @@ use windmill::services::database::get_hasura_pool; use windmill::services::import::import_election_event::{ get_document, get_zip_entries, }; -use windmill::services::tasks_execution::*; -use windmill::services::tasks_execution::{update_complete, update_fail}; +use windmill::services::tasks_execution::{post, update_complete, update_fail}; use windmill::tasks::import_election_event; use windmill::tasks::insert_election_event::{self, CreateElectionEventInput}; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] - +/// Response body for inserting election event. pub struct CreateElectionEventOutput { + /// The election event ID. id: Option, + /// The message. message: Option, + /// The error message. error: Option, + /// The task execution. task_execution: Option, } +/// Insert election event. #[instrument(skip(claims))] #[post("/insert-election-event", format = "json", data = "")] pub async fn insert_election_event_f( @@ -114,14 +119,21 @@ pub async fn insert_election_event_f( } #[derive(Serialize, Deserialize, Debug)] +/// Response body for importing election event. pub struct ImportElectionEventOutput { + /// The election event ID. id: Option, + /// The message. message: Option, + /// The error message. error: Option, + /// The task execution. task_execution: Option, } +/// Import election event. #[instrument(skip(claims))] +#[allow(clippy::too_many_lines)] #[post("/import-election-event", format = "json", data = "")] pub async fn import_election_event_f( body: Json, @@ -191,7 +203,7 @@ pub async fn import_election_event_f( match input.sha256.clone() { Some(hash) if !hash.is_empty() => { match integrity_check(&temp_file_path, hash) { - Ok(_) => { + Ok(()) => { info!("Hash verified !"); } Err(err) => { @@ -236,7 +248,7 @@ pub async fn import_election_event_f( return Ok(Json(ImportElectionEventOutput { id: None, message: None, - error: Some(format!("Error checking import: {:?}", err)), + error: Some(format!("Error checking import: {err:?}")), task_execution: Some(task_execution), })); } @@ -263,7 +275,7 @@ pub async fn import_election_event_f( return Ok(Json(ImportElectionEventOutput { id: None, message: None, - error: Some(format!("Error checking import: {:?}", err)), + error: Some(format!("Error checking import: {err:?}")), task_execution: Some(task_execution), })); } diff --git a/packages/harvest/src/routes/insert_tenant.rs b/packages/harvest/src/routes/insert_tenant.rs index ce408e94caf..37d1742c44e 100644 --- a/packages/harvest/src/routes/insert_tenant.rs +++ b/packages/harvest/src/routes/insert_tenant.rs @@ -12,22 +12,30 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_fail}; use windmill::tasks; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for creating a tenant. pub struct CreateTenantInput { + /// The slug of the tenant. slug: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for creating a tenant. pub struct CreateTenantOutput { + /// The ID of the tenant. id: String, + /// The slug of the tenant. slug: String, + /// The error message. error_msg: Option, + /// The task execution. task_execution: TasksExecution, } +/// Insert tenant. #[instrument(skip(claims))] #[post("/insert-tenant", format = "json", data = "")] pub async fn insert_tenant( @@ -63,7 +71,7 @@ pub async fn insert_tenant( ) .await; return Err(error); - }; + } let celery_app = get_celery_app().await; diff --git a/packages/harvest/src/routes/keys_ceremony.rs b/packages/harvest/src/routes/keys_ceremony.rs index 7060a1e2134..3887d99828c 100644 --- a/packages/harvest/src/routes/keys_ceremony.rs +++ b/packages/harvest/src/routes/keys_ceremony.rs @@ -28,18 +28,24 @@ use windmill::services::database::get_hasura_pool; //////////////////////////////////////////////////////////////////////////////// #[derive(Serialize, Deserialize, Debug)] +/// Request body for checking a private key. pub struct CheckPrivateKeyInput { + /// The election event ID. election_event_id: String, + /// The keys ceremony ID. keys_ceremony_id: String, + /// The private key base64. private_key_base64: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for checking a private key. pub struct CheckPrivateKeyOutput { + /// The validity of the private key. is_valid: bool, } -// The main function to get the private key +/// The main function to get the private key #[instrument(skip(claims))] #[post("/check-private-key", format = "json", data = "")] pub async fn check_private_key( @@ -59,12 +65,12 @@ pub async fn check_private_key( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let is_valid = keys_ceremony::check_private_key( &hasura_transaction, @@ -75,7 +81,7 @@ pub async fn check_private_key( input.private_key_base64.clone(), ) .await - .map_err(|e| (Status::BadRequest, format!("{:?}", e)))?; + .map_err(|e| (Status::BadRequest, format!("{e:?}")))?; event!( Level::INFO, @@ -89,7 +95,7 @@ pub async fn check_private_key( .commit() .await .with_context(|| "error comitting transaction") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(CheckPrivateKeyOutput { is_valid })) } @@ -99,17 +105,22 @@ pub async fn check_private_key( //////////////////////////////////////////////////////////////////////////////// #[derive(Serialize, Deserialize, Debug)] +/// Request body for getting a private key. pub struct GetPrivateKeyInput { + /// The election event ID. election_event_id: String, + /// The keys ceremony ID. keys_ceremony_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for getting a private key. pub struct GetPrivateKeyOutput { + /// The private key base64. private_key_base64: String, } -// The main function to get the private key +/// The main function to get the private key #[instrument(skip(claims))] #[post("/get-private-key", format = "json", data = "")] pub async fn get_private_key( @@ -129,12 +140,12 @@ pub async fn get_private_key( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let encrypted_private_key = keys_ceremony::get_private_key( &hasura_transaction, @@ -144,7 +155,7 @@ pub async fn get_private_key( input.keys_ceremony_id.clone(), ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; event!( Level::INFO, @@ -157,7 +168,7 @@ pub async fn get_private_key( .commit() .await .with_context(|| "error comitting transaction") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(GetPrivateKeyOutput { private_key_base64: encrypted_private_key, @@ -169,28 +180,40 @@ pub async fn get_private_key( //////////////////////////////////////////////////////////////////////////////// #[derive(Serialize, Deserialize, Debug)] +/// Request body for creating a keys ceremony. pub struct CreateKeysCeremonyInput { + /// The election event ID. election_event_id: String, + /// The threshold. threshold: usize, + /// The trustee names. trustee_names: Vec, + /// The election ID. election_id: Option, + /// The name. name: Option, + /// The automatic ceremony flag. is_automatic_ceremony: bool, } #[derive(Debug, Display)] +/// Error enum for creating a keys ceremony. pub enum CreateKeysError { #[strum(serialize = "permission-labels")] + /// Permission labels error. PermissionLabels, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for creating a keys ceremony. pub struct CreateKeysCeremonyOutput { + /// The keys ceremony ID. keys_ceremony_id: String, + /// The error message. error_message: Option, } -// The main function to start a key ceremony +/// The main function to start a key ceremony #[instrument(skip(claims))] #[post("/create-keys-ceremony", format = "json", data = "")] pub async fn create_keys_ceremony( @@ -214,12 +237,12 @@ pub async fn create_keys_ceremony( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let valid_permissions_label = validate_permission_labels( &hasura_transaction, @@ -232,14 +255,14 @@ pub async fn create_keys_ceremony( .map_err(|e| { ( Status::BadRequest, - format!("Error validating permission labels: {:?}", e), + format!("Error validating permission labels: {e:?}"), ) })?; if !valid_permissions_label { error!("User does not have permission labels"); return Ok(Json(CreateKeysCeremonyOutput { - keys_ceremony_id: "".to_string(), + keys_ceremony_id: String::new(), error_message: Some(CreateKeysError::PermissionLabels.to_string()), })); } @@ -257,13 +280,13 @@ pub async fn create_keys_ceremony( input.is_automatic_ceremony, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; hasura_transaction .commit() .await .with_context(|| "error comitting transaction") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; event!( Level::INFO, @@ -279,11 +302,13 @@ pub async fn create_keys_ceremony( } #[derive(Serialize, Deserialize, Debug)] +/// Request body for listing keys ceremonies. pub struct ListKeysCeremonyInput { + /// The election event ID. election_event_id: String, } -// The main function to start a key ceremony +/// The main function to start a key ceremony #[instrument(skip(claims))] #[post("/list-keys-ceremonies", format = "json", data = "")] pub async fn list_keys_ceremonies( @@ -317,12 +342,12 @@ pub async fn list_keys_ceremonies( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let elections = get_elections( &hasura_transaction, @@ -330,16 +355,16 @@ pub async fn list_keys_ceremonies( &input.election_event_id, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let election_permission_labels: Vec<_> = elections .into_iter() .filter_map(|election| election.permission_label) .collect(); - let filtered_labels = if election_permission_labels.len() > 0 { - permission_labels - } else { + let filtered_labels = if election_permission_labels.is_empty() { vec![] + } else { + permission_labels }; let keys_ceremonies = postgres::keys_ceremony::list_keys_ceremony( @@ -349,19 +374,24 @@ pub async fn list_keys_ceremonies( &filtered_labels, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; hasura_transaction .commit() .await .with_context(|| "error comitting transaction") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - let count = keys_ceremonies.len() as i64; + let count = i64::try_from(keys_ceremonies.len()).map_err(|_| { + ( + Status::InternalServerError, + "keys ceremony list length does not fit in i64".to_string(), + ) + })?; Ok(Json(DataList { items: keys_ceremonies, total: TotalAggregate { - aggregate: Aggregate { count: count }, + aggregate: Aggregate { count }, }, })) } diff --git a/packages/harvest/src/routes/limit_access_by_countries.rs b/packages/harvest/src/routes/limit_access_by_countries.rs index 5c8bf465f19..daabef9d7ec 100644 --- a/packages/harvest/src/routes/limit_access_by_countries.rs +++ b/packages/harvest/src/routes/limit_access_by_countries.rs @@ -13,16 +13,22 @@ use tracing::instrument; use windmill::services::limit_access_by_countries::handle_limit_ip_access_by_countries; #[derive(Serialize, Deserialize, Debug)] +/// Request body for limiting access by countries. pub struct LimitAccessByCountriesInput { + /// The voting countries to limit access to. voting_countries: Vec, + /// The enroll countries to limit access to. enroll_countries: Vec, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for limiting access by countries. pub struct LimitAccessByCountriesOutput { + /// The success flag. success: bool, } +/// Limit access by countries. #[instrument(skip(claims))] #[post("/limit-access-by-countries", format = "json", data = "")] pub async fn limit_access_by_countries( diff --git a/packages/harvest/src/routes/miru_plugin.rs b/packages/harvest/src/routes/miru_plugin.rs index 48ee31dd540..094b88b2bdc 100644 --- a/packages/harvest/src/routes/miru_plugin.rs +++ b/packages/harvest/src/routes/miru_plugin.rs @@ -11,7 +11,7 @@ use sequent_core::types::hasura::core::TasksExecution; use sequent_core::types::permissions::Permissions; use serde::{Deserialize, Serialize}; use tracing::{info, instrument}; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_fail}; use windmill::tasks::miru_plugin_tasks::upload_signature_task; use windmill::types::tasks::ETasksExecution; use windmill::{ @@ -25,20 +25,30 @@ use windmill::{ }; #[derive(Serialize, Deserialize, Debug)] +/// Request body for creating a transmission package. pub struct CreateTransmissionPackageInput { + /// The election event ID. election_event_id: String, + /// The election ID. election_id: String, + /// The area ID. area_id: String, + /// The tally session ID. tally_session_id: String, + /// The force flag. force: bool, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for creating a transmission package. pub struct CreateTransmissionPackageOutput { + /// The task execution. task_execution: Option, + /// The error message. error_msg: Option, } +/// Create transmission package for Miru. #[instrument(skip(claims))] #[post("/miru/create-transmission-package", format = "json", data = "")] pub async fn create_transmission_package( @@ -111,15 +121,22 @@ pub async fn create_transmission_package( } #[derive(Serialize, Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for sending a transmission package. pub struct SendTransmissionPackageInput { + /// The election ID. election_id: String, + /// The area ID. area_id: String, + /// The tally session ID. tally_session_id: String, } +/// Response body for sending a transmission package. #[derive(Serialize, Deserialize, Debug)] pub struct SendTransmissionPackageOutput {} +/// Send transmission package for Miru. #[instrument(skip(claims))] #[post("/miru/send-transmission-package", format = "json", data = "")] pub async fn send_transmission_package( @@ -154,17 +171,25 @@ pub async fn send_transmission_package( } #[derive(Serialize, Deserialize, Debug)] +/// Request body for uploading a signature. pub struct UploadSignatureInput { + /// The election ID. election_id: String, + /// The area ID. area_id: String, + /// The tally session ID. tally_session_id: String, + /// The document ID. document_id: String, + /// The password. password: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for uploading a signature. pub struct UploadSignatureOutput {} +/// Upload signature for Miru. #[instrument(skip(claims))] #[post("/miru/upload-signature", format = "json", data = "")] pub async fn upload_signature( @@ -199,7 +224,7 @@ pub async fn upload_signature( .map_err(|err| { ( Status::InternalServerError, - format!("Error creating signature {}", err), + format!("Error creating signature {err}"), ) })?; diff --git a/packages/harvest/src/routes/mod.rs b/packages/harvest/src/routes/mod.rs index 49f0750feea..4d595fab8e4 100644 --- a/packages/harvest/src/routes/mod.rs +++ b/packages/harvest/src/routes/mod.rs @@ -1,57 +1,112 @@ // SPDX-FileCopyrightText: 2025 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only +/// Datafix API endpoints. pub mod api_datafix; +/// Application verification and status management endpoints. pub mod applications; +/// Area management endpoints. pub mod areas; +/// Ballot publication and generation endpoints. pub mod ballot_publication; +/// Ballot publication preview preparation endpoints. pub mod ballot_publication_prepare_preview; +/// Ballot receipt creation endpoints. pub mod create_ballot_receipt; +/// Custom URL management endpoints. pub mod custom_urls; +/// Certificate authority deletion endpoints. pub mod delete_certificate_authority; +/// Election event deletion endpoints. pub mod delete_election_event; +/// Election dates management endpoints. pub mod election_dates; +/// Election event statistics endpoints. pub mod election_event_stats; +/// Per-election statistics (distinct voters, areas, votes per day). pub mod election_stats; +/// Election endpoints. pub mod elections; +/// Electoral log endpoints. pub mod electoral_log; +/// Error catchers endpoints. pub mod error_catchers; +/// Export application endpoints. pub mod export_application; +/// Export ballot publication endpoints. pub mod export_ballot_publication; +/// Export election event endpoints. pub mod export_election_event; +/// Export election event logs endpoints. pub mod export_election_event_logs; +/// Export tally results endpoints. pub mod export_tally_results; +/// Export tasks execution endpoints. pub mod export_tasks_execution; +/// Export template endpoints. pub mod export_template; +/// Export tenant config endpoints. pub mod export_tenant_config; +/// Fetch document endpoints. pub mod fetch_document; +/// Generate preview URL endpoints. pub mod generate_preview_url; +/// Get certificate authorities PEM endpoints. pub mod get_certificate_authorities_pem; +/// Google meet endpoints. pub mod google_meet; +/// Immudb log audit endpoints. pub mod immudb_log_audit; +/// Import application endpoints. pub mod import_application; +/// Import areas endpoints. pub mod import_areas; +/// Import candidates endpoints. pub mod import_candidates; +/// Import certificate authority endpoints. pub mod import_certificate_authority; +/// Import templates endpoints. pub mod import_templates; +/// Import tenant config endpoints. pub mod import_tenant_config; +/// Insert cast vote endpoints. pub mod insert_cast_vote; +/// Insert election event endpoints. pub mod insert_election_event; +/// Insert tenant endpoints. pub mod insert_tenant; +/// Keys ceremony endpoints. pub mod keys_ceremony; +/// Limit access by countries endpoints. pub mod limit_access_by_countries; +/// Miru plugin endpoints. pub mod miru_plugin; +/// Permissions endpoints. pub mod permissions; +/// Plugins endpoints. pub mod plugins; +/// Reports endpoints. pub mod reports; +/// Roles endpoints. pub mod roles; + +/// Scheduled event endpoints. pub mod scheduled_event; +/// Voter authentication endpoints. pub mod set_voter_authentication; +/// Tally ceremony endpoints. pub mod tally_ceremony; +/// Tally sheets endpoints. pub mod tally_sheets; +/// Templates endpoints. pub mod templates; +/// Trustees endpoints. pub mod trustees; +/// Upload document endpoints. pub mod upload_document; +/// Users endpoints. pub mod users; +/// Voter electoral log endpoints. pub mod voter_electoral_log; +/// Voting status endpoints. pub mod voting_status; diff --git a/packages/harvest/src/routes/permissions.rs b/packages/harvest/src/routes/permissions.rs index 4b103a0c7fc..c50375415ac 100644 --- a/packages/harvest/src/routes/permissions.rs +++ b/packages/harvest/src/routes/permissions.rs @@ -17,13 +17,19 @@ use serde::Deserialize; use tracing::instrument; #[derive(Deserialize, Debug)] +/// Request body for getting permissions. pub struct GetPermissionsBody { + /// The tenant ID. tenant_id: String, + /// The search query. search: Option, + /// The limit. limit: Option, + /// The offset. offset: Option, } +/// Get permissions. #[instrument(skip(claims))] #[post("/get-permissions", format = "json", data = "")] pub async fn get_permissions( @@ -40,27 +46,35 @@ pub async fn get_permissions( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let (permissions, count) = client .list_permissions(&realm, input.search, input.limit, input.offset) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + let count_i64 = i64::try_from(count).map_err(|_| { + ( + Status::InternalServerError, + "permission list length does not fit in i64".to_string(), + ) + })?; Ok(Json(DataList { items: permissions, total: TotalAggregate { - aggregate: Aggregate { - count: count as i64, - }, + aggregate: Aggregate { count: count_i64 }, }, })) } #[derive(Deserialize, Debug)] +/// Request body for creating a permission. pub struct CreatePermissionsBody { + /// The tenant ID. tenant_id: String, + /// The permission. permission: Permission, } +/// Create permission in Keycloak realm. #[instrument(skip(claims))] #[post("/create-permission", format = "json", data = "")] pub async fn create_permission( @@ -77,21 +91,26 @@ pub async fn create_permission( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let permission = client .create_permission(&realm, &input.permission) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(permission)) } #[derive(Deserialize, Debug)] +/// Request body for setting or deleting a role permission. pub struct SetOrDeleteRolePermissionsBody { + /// The tenant ID. tenant_id: String, + /// The role ID. role_id: String, + /// The permission name. permission_name: String, } +/// Set role permission in Keycloak realm. #[instrument(skip(claims))] #[post("/set-role-permission", format = "json", data = "")] pub async fn set_role_permission( @@ -108,14 +127,15 @@ pub async fn set_role_permission( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; client .set_role_permission(&realm, &input.role_id, &input.permission_name) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - Ok(Json(Default::default())) + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + Ok(Json(OptionalId::default())) } +/// Delete role permission in Keycloak realm. #[instrument(skip(claims))] #[post("/delete-role-permission", format = "json", data = "")] pub async fn delete_role_permission( @@ -132,20 +152,24 @@ pub async fn delete_role_permission( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; client .delete_role_permission(&realm, &input.role_id, &input.permission_name) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - Ok(Json(Default::default())) + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + Ok(Json(OptionalId::default())) } #[derive(Deserialize, Debug)] +/// Request body for deleting a permission. pub struct DeletePermissionBody { + /// The tenant ID. tenant_id: String, + /// The permission name. permission_name: String, } +/// Delete permission in Keycloak realm. #[instrument(skip(claims))] #[post("/delete-permission", format = "json", data = "")] pub async fn delete_permission( @@ -162,10 +186,10 @@ pub async fn delete_permission( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; client .delete_permission(&realm, &input.permission_name) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - Ok(Json(Default::default())) + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + Ok(Json(OptionalId::default())) } diff --git a/packages/harvest/src/routes/plugins.rs b/packages/harvest/src/routes/plugins.rs index c9e0c2ffc8d..0f7ec6615d8 100644 --- a/packages/harvest/src/routes/plugins.rs +++ b/packages/harvest/src/routes/plugins.rs @@ -19,24 +19,36 @@ use windmill::{ tasks::plugins_tasks::execute_plugin_task, }; #[derive(Deserialize, Debug)] +/// Request body for plugins route. pub struct PluginsRouteInput { + /// The path. path: String, + /// The data in json format. data: Value, + /// The task name to execute. task_execution: Option, + /// The generate document flag. generate_document: Option, } #[derive(Deserialize, Debug, Serialize)] +/// Response body for plugins route. pub struct PluginsRouteOutput { + /// The data in json format. data: Value, } #[derive(Deserialize, Debug, Serialize)] +/// Response body for plugins route task. pub struct PluginsRouteTaskOutput { + /// The document ID. document_id: Option, + /// The task execution. task_execution: TasksExecution, } +/// Generic plugins route mapping. +/// This route is used to execute a plugin task or call a plugin route. #[instrument(skip(claims))] #[post("/plugin", format = "json", data = "")] pub async fn plugin_routes( @@ -59,7 +71,16 @@ pub async fn plugin_routes( let claims_json_string: String = serde_json::to_string(&claims) .expect("Failed to serialize JwtClaims to string"); - route_data["claims"] = serde_json::Value::String(claims_json_string); + { + let route_obj = route_data.as_object_mut().ok_or(( + Status::BadRequest, + "plugin route `data` must be a JSON object".to_string(), + ))?; + route_obj.insert( + "claims".to_string(), + serde_json::Value::String(claims_json_string), + ); + } let task = plugin_manager.get_route_task_handler(&input.path); let task_name = input.task_execution.clone(); @@ -79,16 +100,33 @@ pub async fn plugin_routes( .await .map_err(|e| (Status::InternalServerError, e.to_string()))?; - route_data["task_execution"] = serde_json::Value::String( - serde_json::to_string(&task_execution) - .expect("Failed to serialize task_execution to string"), - ); + { + let route_obj = route_data.as_object_mut().ok_or(( + Status::BadRequest, + "plugin route `data` must be a JSON object".to_string(), + ))?; + route_obj.insert( + "task_execution".to_string(), + serde_json::Value::String( + serde_json::to_string(&task_execution) + .expect("Failed to serialize task_execution to string"), + ), + ); + } let document_id = match input.generate_document { Some(true) => { let doc_id = Uuid::new_v4().to_string(); - route_data["document_id"] = - serde_json::Value::String(doc_id.clone()); + { + let route_obj = route_data.as_object_mut().ok_or(( + Status::BadRequest, + "plugin route `data` must be a JSON object".to_string(), + ))?; + route_obj.insert( + "document_id".to_string(), + serde_json::Value::String(doc_id.clone()), + ); + } Some(doc_id) } _ => None, diff --git a/packages/harvest/src/routes/reports.rs b/packages/harvest/src/routes/reports.rs index 0a0389bd82f..d5a6c940575 100644 --- a/packages/harvest/src/routes/reports.rs +++ b/packages/harvest/src/routes/reports.rs @@ -5,6 +5,7 @@ use crate::services::authorization::authorize; use anyhow::{anyhow, Result}; use deadpool_postgres::Client as DbClient; +use deadpool_postgres::Transaction; use rocket::http::Status; use rocket::serde::json::Json; use sequent_core::{ @@ -15,7 +16,7 @@ use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; use tracing::instrument; use uuid::Uuid; -use windmill::{postgres::reports::Report, services::tasks_execution::*}; +use windmill::{postgres::reports::Report, services::tasks_execution::post}; use windmill::{ postgres::reports::{get_report_by_type, ReportType}, services::reports_vault::get_report_key_pair, @@ -32,18 +33,70 @@ use windmill::{ }; #[derive(Serialize, Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for rendering a document PDF. pub struct RenderDocumentPdfInput { + /// The document ID. pub document_id: String, + /// The election event ID. pub election_event_id: Option, + /// The tally session ID. pub tally_session_id: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for rendering a document PDF. pub struct RenderDocumentPdfResponse { - pub document_id: String, + /// The document ID. + document_id: String, + /// The task execution. pub task_execution: TasksExecution, } +/// Verifies that `document_id` resolves to an HTML document for the tenant. +async fn ensure_document_is_html( + hasura_transaction: &Transaction<'_>, + tenant_id: &str, + election_event_id: Option, + document_id: &str, +) -> Result<(), (Status, String)> { + let Some(found_document) = get_document( + hasura_transaction, + tenant_id, + election_event_id, + document_id, + ) + .await + .map_err(|e| { + ( + Status::InternalServerError, + format!("Error fetching document: {e:?}"), + ) + })? + else { + return Err(( + Status::NotFound, + format!("Document not found: {document_id}"), + )); + }; + + if let Some(media_type) = found_document.media_type { + if media_type != "text/html" { + return Err(( + Status::InternalServerError, + format!("Invalid document type: {media_type}"), + )); + } + } else { + return Err(( + Status::InternalServerError, + format!("Document {document_id}: missing media type"), + )); + } + Ok(()) +} + +/// Render a document PDF. #[instrument(skip(claims))] #[post("/render-document-pdf", format = "json", data = "")] pub async fn render_document_pdf( @@ -76,39 +129,13 @@ pub async fn render_document_pdf( let election_event_id = input.election_event_id.clone(); let document_id = input.document_id.clone(); - let Some(found_document) = get_document( + ensure_document_is_html( &hasura_transaction, &claims.hasura_claims.tenant_id, election_event_id.clone(), &document_id, ) - .await - .map_err(|e| { - ( - Status::InternalServerError, - format!("Error fetching document: {e:?}"), - ) - })? - else { - return Err(( - Status::NotFound, - format!("Document not found: {}", document_id), - )); - }; - - if let Some(media_type) = found_document.media_type { - if "text/html".to_string() != media_type { - return Err(( - Status::InternalServerError, - format!("Invalid document type: {}", media_type), - )); - } - } else { - return Err(( - Status::InternalServerError, - format!("Document {}: missing media type", document_id), - )); - }; + .await?; let executer_name = claims .name @@ -168,11 +195,15 @@ pub async fn render_document_pdf( //////////// #[derive(Serialize, Deserialize, Debug)] +/// Response body for generating a template. pub struct GenerateTemplateResponse { - pub document_id: String, + /// The document ID. + document_id: String, + /// The task execution. pub task_execution: TasksExecution, } +/// Generate a document based on a template. #[instrument(skip(claims))] #[post("/generate-template", format = "json", data = "")] pub async fn generate_template( @@ -251,25 +282,35 @@ pub async fn generate_template( })?; Ok(Json(GenerateTemplateResponse { - document_id: document_id, + document_id, task_execution: task_execution.clone(), })) } #[derive(Serialize, Deserialize, Debug)] +/// Request body for generating a report. pub struct GenerateReportBody { - pub report_id: String, - pub tenant_id: String, - pub report_mode: GenerateReportMode, + /// The report ID. + report_id: String, + /// The tenant ID. + tenant_id: String, + /// The report mode. + report_mode: GenerateReportMode, + /// The election event ID. pub election_event_id: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for generating a report. pub struct GenerateReportResponse { - pub document_id: String, + /// The document ID. + document_id: String, + /// The encryption policy. pub encryption_policy: EReportEncryption, + /// The task execution. pub task_execution: TasksExecution, } +/// Generate a report. #[instrument(skip(claims))] #[post("/generate-report", format = "json", data = "")] pub async fn generate_report( @@ -360,24 +401,32 @@ pub async fn generate_report( })?; Ok(Json(GenerateReportResponse { - document_id: document_id, + document_id, encryption_policy: report.encryption_policy, task_execution: task_execution.clone(), })) } #[derive(Serialize, Deserialize, Debug)] +/// Request body for encrypting a report. pub struct EncryptReportBody { + /// The election event ID. election_event_id: String, + /// The report ID. report_id: Option, + /// The password. password: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for encrypting a report. pub struct ExportTemplateOutput { + /// The document ID. document_id: String, + /// The error message. error_msg: Option, } +/// Encrypt a report. #[instrument(skip(claims))] #[post("/encrypt-report", format = "json", data = "")] pub async fn encrypt_report_route( @@ -398,12 +447,12 @@ pub async fn encrypt_report_route( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; get_report_key_pair( &hasura_transaction, diff --git a/packages/harvest/src/routes/roles.rs b/packages/harvest/src/routes/roles.rs index ea93d7ec436..fbc2f4585d5 100644 --- a/packages/harvest/src/routes/roles.rs +++ b/packages/harvest/src/routes/roles.rs @@ -16,12 +16,21 @@ use sequent_core::types::permissions::Permissions; use serde::Deserialize; use tracing::{event, instrument, Level}; +/// Request body for [`create_role`]. #[derive(Deserialize, Debug)] pub struct CreateRoleBody { + /// Tenant identifier used to resolve the Keycloak realm. tenant_id: String, + /// Role definition to create in the tenant realm. role: Role, } +/// Creates a group-backed role in the tenant Keycloak realm. +/// +/// # Errors +/// +/// Returns [`Status::Unauthorized`] when the caller lacks permission, or +/// [`Status::InternalServerError`] when Keycloak requests fail. #[instrument(skip(claims))] #[post("/create-role", format = "json", data = "")] pub async fn create_role( @@ -36,50 +45,65 @@ pub async fn create_role( vec![Permissions::ROLE_READ], )?; let realm = get_tenant_realm(&input.tenant_id); - let client = KeycloakAdminClient::new() + let kc_create = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - let role = client.create_role(&realm, &input.role).await.map_err(|e| { - event!(Level::INFO, "Error {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) - })?; - //client moved to create_role so need to create new one - let client = KeycloakAdminClient::new() + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + let role = + kc_create + .create_role(&realm, &input.role) + .await + .map_err(|e| { + event!(Level::INFO, "Error {e:?}"); + (Status::InternalServerError, format!("{e:?}")) + })?; + let kc_lookup = KeycloakAdminClient::new() + .await + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + let role_with_id = kc_lookup + .get_role_by_name(&realm, &role) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - let role_with_id = - client.get_role_by_name(&realm, &role).await.map_err(|e| { - event!(Level::INFO, "Error {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) + .map_err(|e| { + event!(Level::INFO, "Error {e:?}"); + (Status::InternalServerError, format!("{e:?}")) })?; - let client = KeycloakAdminClient::new() + let kc_permissions = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - match (role.clone().permissions, role_with_id.id) { - (Some(permissions), Some(id)) => { - client - .set_role_permissions(&realm, &id, &permissions) - .await - .map_err(|e| { - event!(Level::INFO, "Error {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) - })?; - } - _ => {} + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + if let (Some(permissions), Some(id)) = + (role.clone().permissions, role_with_id.id) + { + kc_permissions + .set_role_permissions(&realm, &id, &permissions) + .await + .map_err(|e| { + event!(Level::INFO, "Error {e:?}"); + (Status::InternalServerError, format!("{e:?}")) + })?; } Ok(Json(role)) } +/// Request body for [`get_roles`]. #[derive(Deserialize, Debug)] pub struct GetRolesBody { + /// Tenant identifier used to resolve the Keycloak realm. tenant_id: String, + /// Optional substring filter applied to role names. search: Option, + /// Maximum number of roles to return. limit: Option, + /// Number of roles to skip before collecting results. offset: Option, } +/// Lists roles defined in the tenant Keycloak realm. +/// +/// # Errors +/// +/// Returns [`Status::Unauthorized`] when the caller lacks permission, or +/// [`Status::InternalServerError`] when Keycloak requests fail. #[instrument(skip(claims))] #[post("/get-roles", format = "json", data = "")] pub async fn get_roles( @@ -96,28 +120,38 @@ pub async fn get_roles( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let (roles, count) = client .list_roles(&realm, input.search, input.limit, input.offset) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + let count_i64 = i64::try_from(count).map_err(|_| { + ( + Status::InternalServerError, + "role list length does not fit in i64".to_string(), + ) + })?; Ok(Json(DataList { items: roles, total: TotalAggregate { - aggregate: Aggregate { - count: count as i64, - }, + aggregate: Aggregate { count: count_i64 }, }, })) } #[derive(Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for listing user roles. pub struct ListUserRolesBody { + /// The tenant ID. tenant_id: String, + /// The user ID. user_id: String, + /// The election event ID. election_event_id: Option, } +/// Lists user roles defined in the tenant Keycloak realm. #[instrument(skip(claims))] #[post("/list-user-roles", format = "json", data = "")] pub async fn list_user_roles( @@ -144,21 +178,27 @@ pub async fn list_user_roles( }; let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let roles = client .list_user_roles(&realm, &input.user_id) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(roles)) } #[derive(Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for setting or deleting a user role. pub struct SetOrDeleteUserRoleBody { + /// The tenant ID. tenant_id: String, + /// The user ID. user_id: String, + /// The role ID. role_id: String, } +/// Sets a user role in the tenant Keycloak realm. #[instrument(skip(claims))] #[post("/set-user-role", format = "json", data = "")] pub async fn set_user_role( @@ -175,14 +215,15 @@ pub async fn set_user_role( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; client .set_user_role(&realm, &input.user_id, &input.role_id) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - Ok(Json(Default::default())) + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + Ok(Json(OptionalId::default())) } +/// Deletes a user role in the tenant Keycloak realm. #[instrument(skip(claims))] #[post("/delete-user-role", format = "json", data = "")] pub async fn delete_user_role( @@ -199,20 +240,25 @@ pub async fn delete_user_role( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; client .delete_user_role(&realm, &input.user_id, &input.role_id) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - Ok(Json(Default::default())) + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + Ok(Json(OptionalId::default())) } #[derive(Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for deleting a role. pub struct DeleteRoleBody { + /// The tenant ID. tenant_id: String, + /// The role ID. role_id: String, } +/// Deletes a role in the tenant Keycloak realm. #[instrument(skip(claims))] #[post("/delete-role", format = "json", data = "")] pub async fn delete_role( @@ -229,10 +275,10 @@ pub async fn delete_role( let realm = get_tenant_realm(&input.tenant_id); let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; client .delete_role(&realm, &input.role_id) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; - Ok(Json(Default::default())) + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; + Ok(Json(OptionalId::default())) } diff --git a/packages/harvest/src/routes/scheduled_event.rs b/packages/harvest/src/routes/scheduled_event.rs index ae3cd0ef914..f17fac92b1f 100644 --- a/packages/harvest/src/routes/scheduled_event.rs +++ b/packages/harvest/src/routes/scheduled_event.rs @@ -7,7 +7,7 @@ use crate::services::authorization::authorize; use sequent_core::services::connection; use sequent_core::services::jwt::JwtClaims; use sequent_core::types::permissions::Permissions; -use sequent_core::types::scheduled_event::*; +use sequent_core::types::scheduled_event::EventProcessors; use anyhow::Result; use rocket::http::Status; @@ -20,19 +20,28 @@ use tracing::instrument; use crate::services; #[derive(Deserialize, Debug, Clone)] +/// Request body for creating a scheduled event. pub struct CreateEventBody { + /// The tenant ID. pub tenant_id: String, + /// The election event ID. pub election_event_id: Option, + /// The event processor. pub event_processor: EventProcessors, + /// The cron configuration. pub cron_config: Option, + /// The event payload. pub event_payload: Value, } #[derive(Serialize, Deserialize, Debug, Clone)] +/// Response body for creating a scheduled event. pub struct CreateEventOutput { + /// The ID of the scheduled event. pub id: String, } +/// Creates a scheduled event. #[instrument(skip(claims))] #[post("/scheduled-event", format = "json", data = "")] pub async fn create_scheduled_event( @@ -59,12 +68,12 @@ pub async fn create_scheduled_event( )?; } _ => {} - }; + } let element_id = services::worker::process_scheduled_event(input.clone(), claims) .await - .map_err(|e| (Status::BadRequest, format!("{:?}", e)))?; + .map_err(|e| (Status::BadRequest, format!("{e:?}")))?; Ok(Json(CreateEventOutput { id: element_id })) } diff --git a/packages/harvest/src/routes/set_voter_authentication.rs b/packages/harvest/src/routes/set_voter_authentication.rs index 08bf2dec14e..53f055b0720 100644 --- a/packages/harvest/src/routes/set_voter_authentication.rs +++ b/packages/harvest/src/routes/set_voter_authentication.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only +#![allow(clippy::large_futures)] use crate::services::authorization::authorize; use anyhow::Result; @@ -19,18 +20,26 @@ use windmill::tasks::manage_election_event_enrollment::{ }; #[derive(Serialize, Deserialize, Debug)] +/// Request body for setting voter authentication. pub struct SetVoterAuthentication { + /// The election event ID. pub election_event_id: String, + /// If enrollment is enabled. pub enrollment: String, + /// If OTP is enabled. pub otp: String, } #[derive(Serialize)] -struct SetVoterAuthenticationOutput { +/// Response body for setting voter authentication. +pub struct SetVoterAuthenticationOutput { + /// Whether the operation was successful. success: bool, + /// The message. message: String, } +/// Sets voter authentication. #[instrument(skip(claims))] #[post("/set-voter-authentication", format = "json", data = "")] pub async fn set_voter_authentication( @@ -47,20 +56,20 @@ pub async fn set_voter_authentication( vec![], ) .map_err(|err| { - error!("Authorization failed: {:?}", err); + error!("Authorization failed: {err:?}"); (Status::Forbidden, "Authorization failed".to_string()) })?; let mut hasura_db_client = get_hasura_pool().await.get().await.map_err(|e| { - error!("Failed to get DB pool: {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) + error!("Failed to get DB pool: {e:?}"); + (Status::InternalServerError, format!("{e:?}")) })?; let hasura_transaction = hasura_db_client.transaction().await.map_err(|e| { - error!("Failed to start transaction: {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) + error!("Failed to start transaction: {e:?}"); + (Status::InternalServerError, format!("{e:?}")) })?; let election_event = get_election_event_by_id( @@ -70,8 +79,8 @@ pub async fn set_voter_authentication( ) .await .map_err(|e| { - error!("Failed to fetch election event: {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) + error!("Failed to fetch election event: {e:?}"); + (Status::InternalServerError, format!("{e:?}")) })?; // Extract or set default enrollment and OTP values @@ -109,7 +118,7 @@ pub async fn set_voter_authentication( ) .await .map_err(|error| { - error!("Failed to update enrollment: {:?}", error); + error!("Failed to update enrollment: {error:?}"); ( Status::InternalServerError, format!("Error updating enrollment: {error:?}"), @@ -133,7 +142,7 @@ pub async fn set_voter_authentication( ) .await .map_err(|error| { - error!("Failed to update OTP: {:?}", error); + error!("Failed to update OTP: {error:?}"); ( Status::InternalServerError, format!("Error updating OTP: {error:?}"), @@ -143,8 +152,8 @@ pub async fn set_voter_authentication( // Commit transaction hasura_transaction.commit().await.map_err(|e| { - error!("Transaction commit failed: {:?}", e); - (Status::InternalServerError, format!("{:?}", e)) + error!("Transaction commit failed: {e:?}"); + (Status::InternalServerError, format!("{e:?}")) })?; Ok(Json(SetVoterAuthenticationOutput { diff --git a/packages/harvest/src/routes/tally_ceremony.rs b/packages/harvest/src/routes/tally_ceremony.rs index 14682c34c64..40a695a864d 100644 --- a/packages/harvest/src/routes/tally_ceremony.rs +++ b/packages/harvest/src/routes/tally_ceremony.rs @@ -31,19 +31,26 @@ use windmill::services::database::get_hasura_pool; use windmill::services::providers::transactions_provider::provide_hasura_transaction; #[derive(Serialize, Deserialize, Debug)] +/// Request body for creating a tally ceremony. pub struct CreateTallyCeremonyInput { + /// The election event ID. election_event_id: String, + /// The election IDs. election_ids: Vec, + /// The configuration. configuration: Option, + /// The tally type. tally_type: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for creating a tally ceremony. pub struct CreateTallyCeremonyOutput { + /// The tally session ID. tally_session_id: String, } -// The main function to start a key ceremony +/// The main function to start a key ceremony #[instrument(skip(claims))] #[post("/create-tally-ceremony", format = "json", data = "")] pub async fn create_tally_ceremony( @@ -93,9 +100,9 @@ pub async fn create_tally_ceremony( username, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - let _commit = hasura_transaction.commit().await.map_err(|err| { + hasura_transaction.commit().await.map_err(|err| { (Status::InternalServerError, format!("Commit failed: {err}")) })?; event!( @@ -110,13 +117,19 @@ pub async fn create_tally_ceremony( } #[derive(Serialize, Deserialize, Debug)] +/// Request body for updating a tally ceremony. pub struct UpdateTallyCeremonyInput { + /// The election event ID. election_event_id: String, + /// The tally session ID. tally_session_id: String, + /// The status. status: TallyExecutionStatus, } +/// Updates a tally ceremony. #[instrument(skip(claims))] +#[allow(clippy::too_many_lines)] #[post("/update-tally-ceremony", format = "json", data = "")] pub async fn update_tally_ceremony( body: Json, @@ -245,7 +258,7 @@ pub async fn update_tally_ceremony( .map_err(|e| { ( Status::InternalServerError, - format!("Error with update_tally_ceremony: {:?}", e), + format!("Error with update_tally_ceremony: {e:?}"), ) })?; @@ -263,18 +276,24 @@ pub async fn update_tally_ceremony( //////////////////////////////////////////////////////////////////////////////// #[derive(Serialize, Deserialize, Debug)] +/// Request body for restoring a private key. pub struct SetPrivateKeyInput { + /// The election event ID. election_event_id: String, + /// The private key base64. private_key_base64: String, + /// The tally session ID. tally_session_id: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for restoring a private key. pub struct SetPrivateKeyOutput { + /// Whether the private key is valid. is_valid: bool, } -// The main function to restore the private key +/// The main function to restore the private key #[instrument(skip(claims))] #[post("/restore-private-key", format = "json", data = "")] pub async fn restore_private_key( @@ -315,8 +334,7 @@ pub async fn restore_private_key( &input.private_key_base64, ) .await - .map_err(|e| (Status::BadRequest, format!("{:?}", e)))?; - + .map_err(|e| (Status::BadRequest, format!("{e:?}")))?; event!( Level::INFO, "Restoring given private key, election_event_id={}, tally_session_id={}, is_valid={}", @@ -332,16 +350,24 @@ pub async fn restore_private_key( } #[derive(Serialize, Deserialize, Debug)] +/// Request body for submitting a tally resolution. pub struct SubmitTallyResolutionInput { + /// The election event ID. election_event_id: String, + /// The tally session ID. tally_session_id: String, + /// The resolutions. resolutions: Vec, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for submitting a tally resolution. pub struct SubmitTallyResolutionOutput { + /// Whether the submission was successful. success: bool, + /// The tally session ID. tally_session_id: String, + /// The number of resolutions submitted. resolved_count: usize, } @@ -397,7 +423,6 @@ pub async fn submit_tally_resolution( ) .await .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - hasura_transaction.commit().await.map_err(|err| { (Status::InternalServerError, format!("Commit failed: {err}")) })?; diff --git a/packages/harvest/src/routes/tally_sheets.rs b/packages/harvest/src/routes/tally_sheets.rs index 3843fa90faf..e2ff22b0c7a 100644 --- a/packages/harvest/src/routes/tally_sheets.rs +++ b/packages/harvest/src/routes/tally_sheets.rs @@ -14,19 +14,25 @@ use tracing::instrument; use windmill::postgres::tally_sheet; use windmill::services::database::get_hasura_pool; +/// Request body for [`publish_tally_sheet`]. #[derive(Serialize, Deserialize, Debug)] pub struct PublishTallySheetInput { + /// Election event that owns the tally sheet. election_event_id: String, + /// Identifier of the tally sheet row to publish or unpublish. tally_sheet_id: String, + /// When true, publishes the sheet; when false, unpublishes it. publish: bool, } +/// Response body for [`publish_tally_sheet`]. #[derive(Serialize, Deserialize, Debug)] pub struct PublishTallySheetOutput { + /// Present when a tally sheet row was updated. tally_sheet_id: Option, } -// The main function to start a key ceremony +/// The main function to start a key ceremony #[instrument(skip(claims))] #[post("/publish-tally-sheet", format = "json", data = "")] pub async fn publish_tally_sheet( @@ -45,12 +51,12 @@ pub async fn publish_tally_sheet( .await .get() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let found = tally_sheet::publish_tally_sheet( &hasura_transaction, @@ -61,9 +67,9 @@ pub async fn publish_tally_sheet( input.publish, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - if let None = found { + if found.is_none() { return Ok(Json(PublishTallySheetOutput { tally_sheet_id: None, })); @@ -73,7 +79,7 @@ pub async fn publish_tally_sheet( .commit() .await .with_context(|| "error comitting transaction") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(PublishTallySheetOutput { tally_sheet_id: Some(input.tally_sheet_id.clone()), diff --git a/packages/harvest/src/routes/templates.rs b/packages/harvest/src/routes/templates.rs index f8ef8cccd3d..1867afe4da4 100644 --- a/packages/harvest/src/routes/templates.rs +++ b/packages/harvest/src/routes/templates.rs @@ -12,17 +12,23 @@ use serde::{Deserialize, Serialize}; use tracing::instrument; use windmill::services::reports::utils::get_public_asset_template; +/// Request body for [`get_user_template`]. #[derive(Deserialize, Debug)] pub struct GetUserTemplateBody { + /// Logical template name (e.g. report type key). template_type: String, } +/// Response body for [`get_user_template`]. #[derive(Serialize, Deserialize, Debug)] pub struct GetUserTemplateResponse { + /// Handlebars source for the user-facing template. template_hbs: String, + /// JSON configuration shipped alongside the template. extra_config: String, } +/// Loads bundled user template assets from public storage. #[instrument(skip(claims))] #[post("/get-user-template", format = "json", data = "")] pub async fn get_user_template( diff --git a/packages/harvest/src/routes/trustees.rs b/packages/harvest/src/routes/trustees.rs index 31849a65c13..43c64f90e15 100644 --- a/packages/harvest/src/routes/trustees.rs +++ b/packages/harvest/src/routes/trustees.rs @@ -13,22 +13,28 @@ use serde::{Deserialize, Serialize}; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::{post, update_fail}; use windmill::tasks::export_election_event::{self, ExportOptions}; use windmill::tasks::export_trustees; use windmill::types::tasks::ETasksExecution; #[derive(Serialize, Deserialize, Debug)] +/// Request body for exporting trustees. pub struct ExportTrusteesInput { + /// Password used to protect the generated trustees export. password: String, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for exporting trustees. pub struct ExportTrusteesOutput { + /// The document ID. document_id: String, + /// The task execution. task_execution: TasksExecution, } +/// Exports trustees. #[instrument(skip(claims))] #[post("/export-trustees", format = "json", data = "")] pub async fn export_trustees_route( @@ -75,7 +81,7 @@ pub async fn export_trustees_route( ) })?; return Err(error); - }; + } let document_id = Uuid::new_v4().to_string(); let celery_app = get_celery_app().await; diff --git a/packages/harvest/src/routes/upload_document.rs b/packages/harvest/src/routes/upload_document.rs index 546be2b2ae2..be038fb4109 100644 --- a/packages/harvest/src/routes/upload_document.rs +++ b/packages/harvest/src/routes/upload_document.rs @@ -15,20 +15,31 @@ use windmill::services::{ providers::transactions_provider::provide_hasura_transaction, }; #[derive(Serialize, Deserialize, Debug)] +/// Request body for uploading a document. pub struct UploadDocumentInput { + /// The name. name: String, + /// The media type. media_type: String, + /// The size. size: usize, + /// Whether the document is public. is_public: bool, + /// Whether the document is local. is_local: Option, + /// The election event ID. election_event_id: Option, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for uploading a document. pub struct UploadDocumentOutput { + /// The document ID. document_id: String, + /// The URL of the document. url: String, } +/// Uploads a document and returns the upload URL. #[instrument(skip(claims))] #[post("/get-upload-url", format = "json", data = "")] pub async fn get_upload_url( @@ -70,7 +81,7 @@ pub async fn get_upload_url( inner.election_event_id, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; hasura_transaction.commit().await.map_err(|err| { ( diff --git a/packages/harvest/src/routes/users.rs b/packages/harvest/src/routes/users.rs index ced9f946004..bc31013b41e 100644 --- a/packages/harvest/src/routes/users.rs +++ b/packages/harvest/src/routes/users.rs @@ -21,7 +21,7 @@ use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::env; -use tracing::instrument; +use tracing::{info, instrument}; use uuid::Uuid; use windmill::postgres::election_event::{ get_election_event_by_id, ElectionEventDatafix, @@ -36,7 +36,7 @@ use windmill::services::export::export_users::{ ExportBody, ExportTenantUsersBody, ExportUsersBody, }; use windmill::services::keycloak_events::list_keycloak_events_by_type; -use windmill::services::tasks_execution::*; +use windmill::services::tasks_execution::post; use windmill::services::users::list_users_has_voted; use windmill::services::users::{ count_keycloak_users, list_users, list_users_with_vote_info, @@ -47,12 +47,18 @@ use windmill::tasks::import_users::{self, ImportUsersOutput}; use windmill::types::tasks::ETasksExecution; #[derive(Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for deleting a user. pub struct DeleteUserBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: Option, + /// The user ID. user_id: String, } +/// Deletes a user. #[instrument(skip(claims))] #[post("/delete-user", format = "json", data = "")] pub async fn delete_user( @@ -80,7 +86,7 @@ pub async fn delete_user( let client = KeycloakAdminClient::new().await.map_err(|e| { ( Status::InternalServerError, - format!("Error obtaining the client: {:?}", e), + format!("Error obtaining the client: {e:?}"), ) })?; client @@ -89,19 +95,25 @@ pub async fn delete_user( .map_err(|e| { ( Status::InternalServerError, - format!("Error deleting the user: {:?}", e), + format!("Error deleting the user: {e:?}"), ) })?; - Ok(Json(Default::default())) + Ok(Json(OptionalId::default())) } #[derive(Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields +/// Request body for deleting multiple users. pub struct DeleteUsersBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: Option, + /// The user IDs. users_id: Vec, } +/// Deletes multiple users. #[instrument(skip(claims))] #[post("/delete-users", format = "json", data = "")] pub async fn delete_users( @@ -128,43 +140,64 @@ pub async fn delete_users( }; let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; for id in input.users_id { client .delete_user(&realm, &id) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; } - Ok(Json(Default::default())) + Ok(Json(OptionalId::default())) } #[derive(Deserialize, Debug)] +/// Request body for getting users. pub struct GetUsersBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: Option, + /// The election ID. election_id: Option, + /// The search text. search: Option, + /// The first name filter. first_name: Option, + /// The last name filter. last_name: Option, + /// The username filter. username: Option, + /// The email filter. email: Option, + /// The limit. limit: Option, + /// The offset. offset: Option, + /// Whether to show votes info. show_votes_info: Option, + /// The attributes to filter by. attributes: Option>, + /// Whether the email is verified. email_verified: Option, + /// Whether the user is enabled. enabled: Option, + /// The sort order. sort: Option>, + /// Whether the user has voted. has_voted: Option, + /// The authorized election alias of the user to filter by. authorized_to_election_alias: Option, } #[derive(Deserialize, Debug, Serialize)] +/// Response body for counting users. pub struct CountUserOutput { + /// The count of users. count: i64, } +/// Counts users. #[instrument(skip(claims), ret)] #[post("/count-users", format = "json", data = "")] pub async fn count_users( @@ -186,7 +219,7 @@ pub async fn count_users( let realm = match input.election_event_id { Some(ref election_event_id) => { - get_event_realm(&input.tenant_id, &election_event_id) + get_event_realm(&input.tenant_id, election_event_id) } None => get_tenant_realm(&input.tenant_id), }; @@ -195,28 +228,28 @@ pub async fn count_users( get_keycloak_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring keycloak db client from pool {:?}", e), + format!("Error acquiring keycloak db client from pool {e:?}"), ) })?; let keycloak_transaction = keycloak_db_client.transaction().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring keycloak transaction {:?}", e), + format!("Error acquiring keycloak transaction {e:?}"), ) })?; let mut hasura_db_client: DbClient = get_hasura_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring hasura db client from pool {:?}", e), + format!("Error acquiring hasura db client from pool {e:?}"), ) })?; let hasura_transaction = hasura_db_client.transaction().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring hasura transaction {:?}", e), + format!("Error acquiring hasura transaction {e:?}"), ) })?; @@ -251,7 +284,7 @@ pub async fn count_users( .map_err(|e| { ( Status::InternalServerError, - format!("Error counting users {:?}", e), + format!("Error counting users {e:?}"), ) })?; @@ -260,7 +293,9 @@ pub async fn count_users( })) } +/// Gets users. #[instrument(skip(claims), ret)] +#[allow(clippy::too_many_lines)] #[post("/get-users", format = "json", data = "")] pub async fn get_users( claims: jwt::JwtClaims, @@ -281,7 +316,7 @@ pub async fn get_users( let realm = match input.election_event_id { Some(ref election_event_id) => { - get_event_realm(&input.tenant_id, &election_event_id) + get_event_realm(&input.tenant_id, election_event_id) } None => get_tenant_realm(&input.tenant_id), }; @@ -290,28 +325,28 @@ pub async fn get_users( get_keycloak_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring keycloak db client from pool {:?}", e), + format!("Error acquiring keycloak db client from pool {e:?}"), ) })?; let keycloak_transaction = keycloak_db_client.transaction().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring keycloak transaction {:?}", e), + format!("Error acquiring keycloak transaction {e:?}"), ) })?; let mut hasura_db_client: DbClient = get_hasura_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring hasura db client from pool {:?}", e), + format!("Error acquiring hasura db client from pool {e:?}"), ) })?; let hasura_transaction = hasura_db_client.transaction().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring hasura transaction {:?}", e), + format!("Error acquiring hasura transaction {e:?}"), ) })?; @@ -348,7 +383,7 @@ pub async fn get_users( .map_err(|e| { ( Status::InternalServerError, - format!("Error listing users that has_voted {:?}", e), + format!("Error listing users that has_voted {e:?}"), ) })?; @@ -356,59 +391,60 @@ pub async fn get_users( items: users, total: TotalAggregate { aggregate: Aggregate { - count: count as i64, + count: i64::from(count), }, }, })); } - let (users, count) = match input.show_votes_info.unwrap_or(false) { - true => - // If show_vote_info is true, call list_users_with_vote_info() - { - list_users_with_vote_info( - &hasura_transaction, - &keycloak_transaction, - filter, + let (users, count) = if input.show_votes_info.unwrap_or(false) { + list_users_with_vote_info( + &hasura_transaction, + &keycloak_transaction, + filter, + ) + .await + .map_err(|e| { + ( + Status::InternalServerError, + format!("Error listing users with vote info {e:?}"), ) + })? + } else { + list_users(&hasura_transaction, &keycloak_transaction, filter) .await .map_err(|e| { ( Status::InternalServerError, - format!("Error listing users with vote info {:?}", e), + format!("Error listing users {e:?}"), ) })? - } - // If show_vote_info is false, call list_users() and return empty - // votes_info - false => list_users(&hasura_transaction, &keycloak_transaction, filter) - .await - .map_err(|e| { - ( - Status::InternalServerError, - format!("Error listing users {:?}", e), - ) - })?, }; Ok(Json(DataList { items: users, total: TotalAggregate { aggregate: Aggregate { - count: count as i64, + count: i64::from(count), }, }, })) } #[derive(Deserialize, Debug)] +/// Request body for creating a user. pub struct CreateUserBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: Option, + /// The user. user: User, + /// The user roles IDs. user_roles_ids: Option>, } +/// Creates a user. #[instrument(skip(claims))] #[post("/create-user", format = "json", data = "")] pub async fn create_user( @@ -418,7 +454,7 @@ pub async fn create_user( let input = body.into_inner(); let mut required_perms = Vec::::new(); if input.election_event_id.is_some() { - required_perms.push(Permissions::VOTER_CREATE) + required_perms.push(Permissions::VOTER_CREATE); } else { required_perms.push(Permissions::USER_CREATE); if let Some(attributes) = &input.user.attributes { @@ -428,7 +464,7 @@ pub async fn create_user( required_perms.push(Permissions::PERMISSION_LABEL_WRITE); } } - }; + } authorize(&claims, true, Some(input.tenant_id.clone()), required_perms)?; let realm = match input.election_event_id.clone() { Some(election_event_id) => { @@ -438,10 +474,10 @@ pub async fn create_user( }; let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; let (tenant_id_attribute, groups) = if input.election_event_id.is_some() { let voter_group_name = env::var("KEYCLOAK_VOTER_GROUP_NAME") - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; ( Some(HashMap::from([( TENANT_ID_ATTR_NAME.to_string(), @@ -483,40 +519,56 @@ pub async fn create_user( let user = client .create_user(&realm, &user, user_attributes, groups) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - match (user.id.clone(), &input.user_roles_ids) { - (Some(id), Some(user_roles_ids)) => { - let res: Vec<_> = user_roles_ids - .into_iter() - .map(|role_id| client.set_user_role(&realm, &id, &role_id)) - .collect(); + if let (Some(id), Some(user_roles_ids)) = + (user.id.clone(), &input.user_roles_ids) + { + let res: Vec<_> = user_roles_ids + .iter() + .map(|role_id| client.set_user_role(&realm, &id, role_id)) + .collect(); - join_all(res).await; - } - _ => (), - }; + join_all(res).await; + } Ok(Json(user)) } #[derive(Deserialize, Debug)] +/// Request body for editing a user. pub struct EditUserBody { + /// The tenant ID. tenant_id: String, + /// The user ID. user_id: String, + /// Whether the user is enabled. enabled: Option, + /// The election event ID. election_event_id: Option, + /// The attributes. attributes: Option>>, + /// The email. email: Option, + /// The first name. first_name: Option, + /// The last name. last_name: Option, + /// The username. username: Option, + /// The password. password: Option, + /// Whether the password is temporary. temporary: Option, } const MOBILE_NUMBER_ATTRIBUTE: &str = "sequent.read-only.mobile-number"; +/// Ensures email, phone, and related profile edits are allowed for the voter realm. +/// +/// # Errors +/// +/// Returns an error when the caller attempts to change protected fields. pub async fn check_edit_email_tlf( client: &KeycloakAdminClient, input: &EditUserBody, @@ -553,14 +605,16 @@ pub async fn check_edit_email_tlf( changes.push("temporary".to_string()); } - if changes.len() > 0 { - return Err(anyhow!("Can't change user properties: {:?}", changes)); + if !changes.is_empty() { + return Err(anyhow!("Can't change user properties: {changes:?}")); } Ok(()) } +/// Edits a user. #[instrument(skip(claims), ret)] +#[allow(clippy::too_many_lines)] #[post("/edit-user", format = "json", data = "")] pub async fn edit_user( claims: jwt::JwtClaims, @@ -598,7 +652,7 @@ pub async fn edit_user( required_perms.push(Permissions::PERMISSION_LABEL_WRITE); } } - }; + } authorize(&claims, true, Some(input.tenant_id.clone()), required_perms)?; let realm = match input.election_event_id.clone() { @@ -612,7 +666,7 @@ pub async fn edit_user( get_hasura_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring hasura db client from pool {:?}", e), + format!("Error acquiring hasura db client from pool {e:?}"), ) })?; @@ -620,15 +674,17 @@ pub async fn edit_user( hasura_db_client.transaction().await.map_err(|e| { ( Status::InternalServerError, - format!("Error acquiring hasura transaction {:?}", e), + format!("Error acquiring hasura transaction {e:?}"), ) })?; // check if the voter has voted if !voter_voted_edit { if let Some(election_event_id) = input.election_event_id.clone() { - let mut user = User::default(); - user.id = Some(input.user_id.clone()); + let user = User { + id: Some(input.user_id.clone()), + ..Default::default() + }; let voters = get_users_with_vote_info( &hasura_transaction, &input.tenant_id, @@ -641,20 +697,21 @@ pub async fn edit_user( .map_err(|e| { ( Status::InternalServerError, - format!("Error listing users with vote info {:?}", e), + format!("Error listing users with vote info {e:?}"), ) })?; let Some(voter) = voters.first() else { return Err(( Status::InternalServerError, - format!("Error listing voter with vote info"), + "Error listing voter with vote info".to_string(), )); }; if let Some(votes_info) = voter.votes_info.clone() { - if votes_info.len() > 0 { + if !votes_info.is_empty() { return Err(( Status::Unauthorized, - format!("Can't edit a voter that has already cast its ballot"), + "Can't edit a voter that has already cast its ballot" + .to_string(), )); } } @@ -663,9 +720,9 @@ pub async fn edit_user( let client = KeycloakAdminClient::new() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - let new_attributes = input.attributes.clone().unwrap_or(HashMap::new()); + let new_attributes = input.attributes.clone().unwrap_or_default(); // maintain current user attributes and do not allow to override tenant-id if new_attributes.contains_key(TENANT_ID_ATTR_NAME) { @@ -678,7 +735,7 @@ pub async fn edit_user( if voter_email_tlf_edit { /*check_edit_email_tlf(&client, &input, &realm, &new_attributes) .await - .map_err(|e| (Status::Unauthorized, format!("{:?}", e)))?;*/ + .map_err(|e| (Status::Unauthorized, format!("{e:?}")))?;*/ } let user = client @@ -695,7 +752,12 @@ pub async fn edit_user( input.temporary, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| { + ( + Status::InternalServerError, + format!("Error editing user {e:?}"), + ) + })?; // If the user is disabled via EDIT: send a SetNotVoted request to // VoterView, it is a Datafix requirement @@ -729,13 +791,19 @@ pub async fn edit_user( Ok(Json(user)) } +/// Request body for [`get_user`]. #[derive(Deserialize, Debug)] +#[allow(clippy::struct_field_names)] // enable same postfix for all fields pub struct GetUserBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: Option, + /// The user ID. user_id: String, } +/// Gets a user. #[instrument(skip(claims))] #[post("/get-user", format = "json", data = "")] pub async fn get_user( @@ -760,17 +828,21 @@ pub async fn get_user( } None => get_tenant_realm(&input.tenant_id), }; - let client = KeycloakAdminClient::new() - .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + let client = KeycloakAdminClient::new().await.map_err(|e| { + ( + Status::InternalServerError, + format!("Error obtaining the client: {e:?}"), + ) + })?; let user = client .get_user(&realm, &input.user_id) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(user)) } +/// Imports users. #[instrument(skip(claims))] #[post("/import-users", format = "json", data = "")] pub async fn import_users_f( @@ -781,7 +853,7 @@ pub async fn import_users_f( let tenant_id = claims.hasura_claims.tenant_id.clone(); let election_event_id = input.election_event_id.clone().unwrap_or_default(); let is_admin = election_event_id.is_empty(); - info!("Calculated is_admin: {}", is_admin); + info!("Calculated is_admin: {is_admin}"); let executer_name = claims .name @@ -819,19 +891,16 @@ pub async fn import_users_f( let mut task_input = input.clone(); task_input.is_admin = is_admin; - let _celery_task = match celery_app + let Ok(_celery_task) = celery_app .send_task(import_users::import_users::new( task_input, task_execution.clone(), )) .await - { - Ok(celery_task) => celery_task, - Err(_) => { - return Ok(Json(ImportUsersOutput { - task_execution: task_execution.clone(), - })); - } + else { + return Ok(Json(ImportUsersOutput { + task_execution: task_execution.clone(), + })); }; info!("Sent IMPORT_USERS task {}", task_execution.id); @@ -843,6 +912,7 @@ pub async fn import_users_f( Ok(Json(output)) } +/// Exports users. #[instrument(skip(claims))] #[post("/export-users", format = "json", data = "")] pub async fn export_users_f( @@ -931,6 +1001,7 @@ pub async fn export_users_f( Ok(Json(output)) } +/// Exports tenant users. #[instrument(skip(claims))] #[post("/export-tenant-users", format = "json", data = "")] pub async fn export_tenant_users_f( @@ -971,7 +1042,7 @@ pub async fn export_tenant_users_f( }; let output = export_users::ExportUsersOutput { - document_id: document_id, + document_id, error_msg: None, task_execution: None, }; @@ -981,11 +1052,15 @@ pub async fn export_tenant_users_f( } #[derive(Deserialize, Debug)] +/// Request body for getting user profile attributes. pub struct GetUserProfileAttributesBody { + /// The tenant ID. tenant_id: String, + /// The election event ID. election_event_id: Option, } +/// Gets user profile attributes for specific realm #[instrument(skip(claims))] #[post("/get-user-profile-attributes", format = "json", data = "")] pub async fn get_user_profile_attributes( @@ -1013,14 +1088,22 @@ pub async fn get_user_profile_attributes( None => get_tenant_realm(&input.tenant_id), }; - let client = KeycloakAdminClient::new() - .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + let client = KeycloakAdminClient::new().await.map_err(|e| { + ( + Status::InternalServerError, + format!("Error creating Keycloak client: {e:?}"), + ) + })?; let attributes_res = client .get_user_profile_attributes(&realm) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| { + ( + Status::InternalServerError, + format!("Error getting user profile attributes: {e:?}"), + ) + })?; Ok(Json(attributes_res)) } diff --git a/packages/harvest/src/routes/voter_electoral_log.rs b/packages/harvest/src/routes/voter_electoral_log.rs index 7ea3e1f2679..306337a7bf6 100644 --- a/packages/harvest/src/routes/voter_electoral_log.rs +++ b/packages/harvest/src/routes/voter_electoral_log.rs @@ -23,16 +23,25 @@ use windmill::services::providers::transactions_provider::provide_hasura_transac use windmill::types::resources::OrderDirection; #[derive(Deserialize, Debug)] +/// Request body for listing cast vote messages. pub struct CastVoteMessagesInput { + /// The tenant ID. pub tenant_id: String, + /// The election event ID. pub election_event_id: String, + /// The election ID. pub election_id: Option, + /// The ballot ID. pub ballot_id: String, + /// The limit. pub limit: Option, + /// The offset. pub offset: Option, + /// The order by. pub order_by: Option>, } +/// Lists cast vote messages. #[instrument] #[post("/list-cast-vote-messages", format = "json", data = "")] pub async fn list_cast_vote_messages( @@ -54,7 +63,7 @@ pub async fn list_cast_vote_messages( .map_err(|e| { ErrorResponse::new( Status::Unauthorized, - &format!("{:?}", e), + &format!("{e:?}"), ErrorCode::Unauthorized, ) })?; // TODO: Temporary till merging the ballot performace inprovements. diff --git a/packages/harvest/src/routes/voting_status.rs b/packages/harvest/src/routes/voting_status.rs index 379c5f6b033..1b532998f5f 100644 --- a/packages/harvest/src/routes/voting_status.rs +++ b/packages/harvest/src/routes/voting_status.rs @@ -16,17 +16,24 @@ use windmill::services::database::get_hasura_pool; use windmill::services::{election_event_status, voting_status}; #[derive(Serialize, Deserialize, Debug)] +/// Request body for updating the voting status of an election event. pub struct UpdateEventVotingStatusInput { + /// The election event ID. pub election_event_id: String, + /// The voting status. pub voting_status: VotingStatus, + /// The voting channels. pub voting_channels: Option>, } #[derive(Serialize, Deserialize, Debug)] +/// Response body for updating the voting status of an election event. pub struct UpdateEventVotingStatusOutput { + /// The election event ID. pub election_event_id: String, } +/// Updates the voting status of an election event. #[instrument(skip(claims))] #[post("/update-event-voting-status", format = "json", data = "")] pub async fn update_event_status( @@ -54,13 +61,13 @@ pub async fn update_event_status( get_hasura_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error getting hasura client {:?}", e), + format!("Error getting hasura client {e:?}"), ) })?; let hasura_transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; election_event_status::update_event_voting_status( &hasura_transaction, @@ -72,18 +79,19 @@ pub async fn update_event_status( &input.voting_channels, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - let _commit = hasura_transaction + hasura_transaction .commit() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(UpdateEventVotingStatusOutput { election_event_id: input.election_event_id.clone(), })) } +/// Updates the voting status of an election. #[instrument(skip(claims))] #[post("/update-election-voting-status", format = "json", data = "")] pub async fn update_election_status( @@ -113,14 +121,14 @@ pub async fn update_election_status( get_hasura_pool().await.get().await.map_err(|e| { ( Status::InternalServerError, - format!("Error getting hasura client {:?}", e), + format!("Error getting hasura client {e:?}"), ) })?; let hasura_transaction: deadpool_postgres::Transaction = hasura_db_client .transaction() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; voting_status::update_election_status( tenant_id, Some(&user_id), @@ -132,12 +140,12 @@ pub async fn update_election_status( &input.voting_channels, ) .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; - let _commit = hasura_transaction + hasura_transaction .commit() .await - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + .map_err(|e| (Status::InternalServerError, format!("{e:?}")))?; Ok(Json(voting_status::UpdateElectionVotingStatusOutput { election_id: input.election_id.clone(), diff --git a/packages/harvest/src/services/mod.rs b/packages/harvest/src/services/mod.rs index befa8fcccf5..2fd8666cfff 100644 --- a/packages/harvest/src/services/mod.rs +++ b/packages/harvest/src/services/mod.rs @@ -2,5 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-only +/// Authorization services. pub mod authorization; +/// Worker services. pub mod worker; diff --git a/packages/harvest/src/services/worker.rs b/packages/harvest/src/services/worker.rs index 700e2ae73be..ede74bfae6a 100644 --- a/packages/harvest/src/services/worker.rs +++ b/packages/harvest/src/services/worker.rs @@ -5,16 +5,17 @@ use crate::routes::scheduled_event; use crate::services::worker::scheduled_event::CreateEventBody; use anyhow::{anyhow, Result}; -use sequent_core::serialization::deserialize_with_path::*; +use sequent_core::serialization::deserialize_with_path::deserialize_value; use sequent_core::services::jwt::JwtClaims; -use sequent_core::types::scheduled_event::*; +use sequent_core::types::scheduled_event::EventProcessors; use sequent_core::types::templates::SendTemplateBody; use tracing::{event, instrument, Level}; use uuid::Uuid; use windmill::services::celery_app::get_celery_app; use windmill::tasks::render_report; -use windmill::tasks::send_template::*; +use windmill::tasks::send_template::send_template; +/// Processes a scheduled event. #[instrument(skip(claims), err)] pub async fn process_scheduled_event( event: CreateEventBody, @@ -52,15 +53,17 @@ pub async fn process_scheduled_event( .await?; event!(Level::INFO, "Sent SEND_TEMPLATE task {}", task.task_id); } - EventProcessors::ALLOW_INIT_REPORT => {} - EventProcessors::START_VOTING_PERIOD => {} - EventProcessors::END_VOTING_PERIOD => {} - EventProcessors::ALLOW_VOTING_PERIOD_END => {} - EventProcessors::START_ENROLLMENT_PERIOD => {} - EventProcessors::END_ENROLLMENT_PERIOD => {} - EventProcessors::START_LOCKDOWN_PERIOD => {} - EventProcessors::END_LOCKDOWN_PERIOD => {} - EventProcessors::ALLOW_TALLY => {} + EventProcessors::ALLOW_INIT_REPORT + | EventProcessors::START_VOTING_PERIOD + | EventProcessors::END_VOTING_PERIOD + | EventProcessors::ALLOW_VOTING_PERIOD_END + | EventProcessors::START_ENROLLMENT_PERIOD + | EventProcessors::END_ENROLLMENT_PERIOD + | EventProcessors::START_LOCKDOWN_PERIOD + | EventProcessors::END_LOCKDOWN_PERIOD + | EventProcessors::ALLOW_TALLY => { + // Nothing to do for these event processors. + } } Ok(element_id) } diff --git a/packages/harvest/src/types/error_response.rs b/packages/harvest/src/types/error_response.rs index 85a8ff7d30b..4a5d9758466 100644 --- a/packages/harvest/src/types/error_response.rs +++ b/packages/harvest/src/types/error_response.rs @@ -9,50 +9,80 @@ use std::convert::AsRef; use strum_macros::{AsRefStr, Display}; use tracing::instrument; +/// JSON error payload type returned by routes using [`ErrorResponse`]. pub type JsonError = Custom>; -#[derive(Serialize, AsRefStr, Display, Debug)] +#[derive(Serialize, AsRefStr, Display, Debug, Copy, Clone)] +/// Error code enum. pub enum ErrorCode { + /// Internal server error. InternalServerError, + /// Unauthorized. Unauthorized, + /// Check status failed. CheckStatusFailed, + /// Area not found. AreaNotFound, + /// Election event not found. ElectionEventNotFound, + /// Electoral log not found. ElectoralLogNotFound, + /// Check previous votes failed. CheckPreviousVotesFailed, + /// Check revotes failed. CheckRevotesFailed, + /// Check votes in other areas failed. CheckVotesInOtherAreasFailed, + /// Insert failed because the voter exceeded allowed revotes. InsertFailedExceedsAllowedRevotes, + /// Get client credentials failed. GetClientCredentialsFailed, + /// Get area ID failed. GetAreaIdFailed, + /// Failed to obtain a database transaction. GetTransactionFailed, + /// Deserialize ballot failed. DeserializeBallotFailed, + /// Deserialize area presentation failed. DeserializeAreaPresentationFailed, + /// Deserialize contests failed. DeserializeContestsFailed, + /// Pok validation failed. PokValidationFailed, + /// UUID parse failed. UuidParseFailed, + /// Unknown error. UnknownError, + /// Invalid event processor. InvalidEventProcessor, + /// Confirm policy show cast vote logs failed. ConfirmPolicyShowCastVoteLogsFailed, + /// Ballot ID mismatch. BallotIdMismatch, // Add any other needed error codes } +/// GraphQL-style `extensions` object attached to [`ErrorResponse`]. #[derive(Serialize)] pub struct ErrorExtensions { + /// Machine-readable error code string. pub code: String, } +/// Structured JSON error body for Rocket `Custom` responses. #[derive(Serialize)] pub struct ErrorResponse { + /// Human-readable error message. pub message: String, + /// Additional error metadata (for example the error code). pub extensions: ErrorExtensions, } impl ErrorResponse { + /// Builds a [`JsonError`] with the given HTTP status, message, and [`ErrorCode`]. #[instrument] pub fn new(status: Status, message: &str, code: ErrorCode) -> JsonError { - return Custom( + Custom( status, Json(ErrorResponse { message: message.into(), @@ -60,6 +90,6 @@ impl ErrorResponse { code: code.as_ref().into(), }, }), - ); + ) } } diff --git a/packages/harvest/src/types/mod.rs b/packages/harvest/src/types/mod.rs index 066492b0905..65ab39d3a98 100644 --- a/packages/harvest/src/types/mod.rs +++ b/packages/harvest/src/types/mod.rs @@ -2,6 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0-only +/// Error response type. pub mod error_response; +/// Optional type. pub mod optional; +/// Resources type. pub mod resources; diff --git a/packages/harvest/src/types/optional.rs b/packages/harvest/src/types/optional.rs index 53dd0aac580..89eff8db0cb 100644 --- a/packages/harvest/src/types/optional.rs +++ b/packages/harvest/src/types/optional.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use std::default::Default; #[derive(Serialize, Deserialize, Debug, Default)] +/// Response body for optional ID. pub struct OptionalId { + /// The ID. pub id: Option, } diff --git a/packages/harvest/src/types/resources.rs b/packages/harvest/src/types/resources.rs index 0cc57943ea6..1dbb9dec32d 100644 --- a/packages/harvest/src/types/resources.rs +++ b/packages/harvest/src/types/resources.rs @@ -7,34 +7,46 @@ use immudb_rs::{sql_value::Value, Client, NamedParam, Row, SqlValue}; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; +/// Single numeric aggregate (for example a total count). #[derive(Serialize, Deserialize, Debug)] pub struct Aggregate { + /// Aggregate value, typically a row count. pub count: i64, } +/// Wrapper for nested aggregates in API list responses. #[derive(Serialize, Deserialize, Debug)] pub struct TotalAggregate { + /// Root aggregate payload. pub aggregate: Aggregate, } -// Enumeration for the valid order directions +/// Enumeration for the valid order directions #[derive(Debug, Deserialize, EnumString, Display)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum OrderDirection { + /// Ascending order. Asc, + /// Descending order. Desc, } #[derive(Deserialize, Debug)] +/// Payload for sorting. pub struct SortPayload { + /// The field to sort by. pub field: String, + /// The order to sort by. pub order: String, } +/// Paginated list envelope with items and total aggregate metadata. #[derive(Serialize, Deserialize, Debug)] pub struct DataList { + /// Items for the current page or query. pub items: Vec, + /// Total counts or related aggregates for the full result set. pub total: TotalAggregate, } @@ -45,9 +57,7 @@ impl TryFrom<&Row> for Aggregate { let mut count = 0; for (column, value) in row.columns.iter().zip(row.values.iter()) { - match column.as_str() { - _ => assign_value!(Value::N, value, count), - } + assign_value!(Value::N, value, count); } Ok(Aggregate { count }) }