Skip to content

Commit

Permalink
Merge pull request #45 from sireto/feat/contacts-UI
Browse files Browse the repository at this point in the history
Feat/contacts UI
  • Loading branch information
saisab29 authored Feb 20, 2025
2 parents 94f412a + 2b9d90d commit 78de00b
Show file tree
Hide file tree
Showing 45 changed files with 4,660 additions and 600 deletions.
735 changes: 653 additions & 82 deletions backend/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ reqwest = { version = "0.12", features = ["json"] }

diesel-derive-enum = {version= "2.1.0", features=["postgres"] }

multipart = "0.18.0"
axum-extra = { version = "0.10.0", features = ["multipart"] }
csv = "1.3.1"



[dev-dependencies]
Expand Down
4 changes: 2 additions & 2 deletions backend/migrations/2025-01-29-055113_list_contact/up.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
CREATE TABLE "list_contacts"(
list_id UUID NOT NULL,
contact_id UUID NOT NULL,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now(),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
PRIMARY KEY (list_id, contact_id),
FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE,
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE
Expand Down
2 changes: 1 addition & 1 deletion backend/migrations/2025-01-31-040420_create_server/up.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
--CREATE TYPE tls_type AS ENUM ('STARTTLS', 'SSL/TLS', 'NONE');
CREATE TYPE tls_type AS ENUM ('STARTTLS', 'SSL/TLS', 'NONE');

CREATE TABLE "servers" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
Expand Down
1 change: 0 additions & 1 deletion backend/src/handlers/campaign_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use crate::{
use axum::{
extract::Path, Json, http::StatusCode
};
use utoipa::ToSchema;

#[utoipa::path(
post,
Expand Down
168 changes: 153 additions & 15 deletions backend/src/handlers/contact.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use crate::{models::contact::
{
Contact,
CreateContactRequest,
CreateContactResponse,
GetContactResponse,
UpdateContactRequest,
UpdateContactResponse,
DeleteContactResponse
Contact, CreateContactRequest, CreateContactResponse, DeleteContactResponse, EmailQuery, GetContactResponse, GetContactResponsee, UpdateContactRequest, UpdateContactResponse, ImportOptions, ImportResponse
}, services::contact
};
use crate::services::contact as contact_service;

use axum::{
extract:: Path, Json, http::StatusCode
extract::{ Path, Query}, http::StatusCode, Json
};

use axum_extra::extract::Multipart;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;

use crate::utils::contact_lists_functions::parse_csv_data;

#[utoipa::path(
post,
path = "/api/contacts",
Expand All @@ -23,13 +24,22 @@ use axum::{
(status = 404)
)
)]
pub async fn create_contact(
Json(payload): Json<CreateContactRequest>,
) -> Result<Json<CreateContactResponse>, (StatusCode, String)> {
pub async fn create_contacts(
Json(payloads): Json<Vec<CreateContactRequest>>, // Corrected JSON extractor
) -> Result<Json<Vec<CreateContactResponse>>, (StatusCode, String)> {

let created_contacts = contact_service::create_contacts(payloads).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "NO Contacts Created".to_string()))?;

let created_contact = contact::create_contact(payload).await?;
let response: Vec<CreateContactResponse> = created_contacts.iter().map(|contact| CreateContactResponse {
id: contact.id,
first_name: contact.first_name.clone(),
last_name: contact.last_name.clone(),
email: contact.email.clone(),
attribute: contact.attribute.clone(),
}).collect();

Ok(Json(created_contact))
Ok(Json(response)) // Wrap response inside Json()
}

#[utoipa::path(
Expand All @@ -40,16 +50,20 @@ pub async fn create_contact(
(status = 404)
)
)]
pub async fn get_contacts() -> Result<Json<Vec<GetContactResponse>>, (StatusCode, String)> {
let contacts = contact_service::get_all_contacts().await?;
pub async fn get_contacts() -> Result<Json<Vec<GetContactResponsee>>, (StatusCode, String)> {
println!("Handler called");
let contacts = contact_service::get_all_contactss().await?;

if contacts.is_empty() {
println!("No contacts found");
return Err((StatusCode::NOT_FOUND, "No contacts found".to_string()));
}

println!("Found {} contacts", contacts.len());
Ok(Json(contacts))
}


#[utoipa::path(
get,
path = "/api/contacts/{contact_id}",
Expand Down Expand Up @@ -109,3 +123,127 @@ pub async fn delete_contact(

Ok(Json(delete_contact_response))
}


#[utoipa::path(
get,
path = "/api/contacts/check-email",
params(
EmailQuery
),
responses(
(status = 200, description = "Email existence check result", body = bool),
(status = 400, description = "Invalid email format"),
(status = 500, description = "Internal server error")
)
)]
pub async fn check_email(
Query(query): Query<EmailQuery>
) -> Result<Json<bool>, (StatusCode, String)> {

match contact::check_email_exists(query.email).await {
Ok(exists) => Ok(Json(exists)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("{:?}", err)
)),
}
}


#[utoipa::path(
post,
path = "/api/contacts/import",
request_body = ImportOptions,
responses(
(status = 200, description = "Import contacts from CSV", body = ImportResponse),
(status = 400, description = "Bad request"),
(status = 500, description = "Internal server error")
)
)]
pub async fn import_contacts(
mut multipart: Multipart,
) -> Result<Json<ImportResponse>, (StatusCode, String)> {
// Default form values
let mut file_data = None;
let mut mode = "subscribe".to_string();
let mut status = "unconfirmed".to_string();
let mut overwrite = false;
let mut delimiter = ",".to_string();
let mut lists_json = None;

// Process multipart form fields
while let Some(field) = multipart.next_field().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Failed to process form: {}", e))
)? {
if let Some(name) = field.name() {
match name {
"file" => {
file_data = Some(field.bytes().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Failed to read file: {}", e))
)?.to_vec());
},
"mode" => {
mode = field.text().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Invalid mode: {}", e))
)?.to_string();
},
"status" => {
status = field.text().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Invalid status: {}", e))
)?.to_string();
},
"overwrite" => {
overwrite = field.text().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Invalid overwrite value: {}", e))
)?.to_lowercase() == "true";
},
"delimiter" => {
delimiter = field.text().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Invalid delimiter: {}", e))
)?.to_string();
},
"lists" => {
lists_json = Some(field.text().await.map_err(|e|
(StatusCode::BAD_REQUEST, format!("Invalid lists data: {}", e))
)?.to_string());
},
_ => {} // Ignore unknown fields
}
}
}

// Ensure file was uploaded
let file_data = file_data.ok_or((
StatusCode::BAD_REQUEST,
"Missing required file upload".to_string()
))?;

// Parse list IDs from JSON
let list_ids = lists_json
.map(|json| serde_json::from_str::<Vec<String>>(&json)
.map(|ids| ids.iter()
.filter_map(|id| Uuid::parse_str(id).ok())
.collect::<Vec<Uuid>>())
.unwrap_or_default())
.unwrap_or_default();

// Parse CSV and import contacts
let contacts = parse_csv_data(
&file_data,
&delimiter,
&mode,
&status,
overwrite,
).map_err(|e| (StatusCode::BAD_REQUEST, format!("CSV parsing error: {}", e)))?;

let result = contact_service::import_contacts(contacts, list_ids, overwrite).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Import failed: {}", e)))?;

Ok(Json(ImportResponse {
success: true,
imported: result.imported,
errors: (!result.errors.is_empty()).then_some(result.errors),
}))
}

49 changes: 48 additions & 1 deletion backend/src/models/contact.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use chrono::{ DateTime, NaiveDateTime, Utc };
use serde_json::Value;
use serde::{ Serialize, Deserialize };
use utoipa::ToSchema;
use utoipa::{IntoParams, ToSchema};
use diesel::prelude::*;
use uuid::Uuid;

Expand Down Expand Up @@ -98,4 +98,51 @@ pub struct DeleteContactResponse {
pub first_name: String,
pub last_name: String,
pub email: String,
}

#[derive(Debug, Deserialize, IntoParams)]
pub struct EmailQuery {
pub email: String
}

#[derive(Debug, Default, Serialize, Deserialize, ToSchema)]
pub struct GetContactResponsee {
#[schema(value_type = String, example = "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8")]
pub id: Uuid,
pub first_name: String,
pub last_name: String,
pub email: String,

#[schema(value_type = String, example = "{\"address\": \"Shinjuku\", \"city\": \"Tokyo\"}")]
pub attribute: Option<Value>,

#[schema(value_type = String, example = "2023-01-01T00:00:00Z")]
pub created_at: DateTime<Utc>,

#[schema(value_type = String, example = "2023-01-01T00:00:00Z")]
pub updated_at: DateTime<Utc>,

pub list_names: Vec<String>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct ImportOptions {
pub mode: Option<String>,
pub status: Option<String>,
pub overwrite: Option<bool>,
pub delimiter: Option<String>,
pub lists: Option<String>, // JSON string of list ids
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ImportResponse {
pub success: bool,
pub imported: usize,
pub errors: Option<Vec<String>>,
}

#[derive(Debug)]
pub struct ImportResult {
pub imported: usize,
pub errors: Vec<String>,
}
13 changes: 13 additions & 0 deletions backend/src/models/list_contacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,16 @@ pub struct NewContactInList {
pub contact_id: Uuid,
}

#[derive(Debug, Default, Serialize, Deserialize, ToSchema, Clone)]
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::list_contacts)]
pub struct ListContact {
#[schema(value_type=String, example="a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8")]
pub list_id: Uuid,
#[schema(value_type=String, example="a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8")]
pub contact_id: Uuid,
#[schema(value_type = String, example = "2023-01-01T00:00:00Z")]
pub created_at: DateTime<Utc>,
#[schema(value_type = String, example = "2023-01-01T00:00:00Z")]
pub updated_at: DateTime<Utc>,
}
Loading

0 comments on commit 78de00b

Please sign in to comment.