From 37181c747b50cb57d27bdc4293ba3a16501a4e8f Mon Sep 17 00:00:00 2001 From: Franklin Date: Sun, 30 Apr 2023 14:32:34 -0400 Subject: [PATCH] backend done for now --- .gitignore | 3 + Cargo.lock | 232 ++++++++++++++++++++++++++++++++++-- Cargo.toml | 9 +- deploy/Dockerfile | 4 + deploy/deploy-to-backend.sh | 7 ++ migrations/1_contact.sql | 7 ++ sql/contact/insert.sql | 5 + src/dao/contact.rs | 16 +++ src/dao/mod.rs | 12 ++ src/domain/mod.rs | 0 src/main.rs | 12 +- src/routes/contact.rs | 16 +++ src/routes/mod.rs | 50 ++++++++ src/services/contact.rs | 17 +++ src/services/mod.rs | 1 + src/utils/macros.rs | 74 ++++++++++++ src/utils/mod.rs | 1 + 17 files changed, 455 insertions(+), 11 deletions(-) create mode 100644 deploy/Dockerfile create mode 100755 deploy/deploy-to-backend.sh create mode 100644 migrations/1_contact.sql create mode 100644 sql/contact/insert.sql create mode 100644 src/dao/contact.rs delete mode 100644 src/domain/mod.rs create mode 100644 src/routes/contact.rs create mode 100644 src/services/contact.rs create mode 100644 src/utils/macros.rs create mode 100644 src/utils/mod.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..4a4149c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +.env +/dist +dist.zip \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1263e81..f12dc9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,29 @@ dependencies = [ "smallvec", ] +[[package]] +name = "actix-files" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + [[package]] name = "actix-http" version = "3.3.1" @@ -198,6 +221,55 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-web-lab" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9f49571dfcf49ed79c6e7a645e9554ae01925eb55fa6e3b2501ceeed24d7e7" +dependencies = [ + "actix-files", + "actix-http", + "actix-router", + "actix-service", + "actix-utils", + "actix-web", + "actix-web-lab-derive", + "ahash 0.8.3", + "arc-swap", + "async-trait", + "bytes", + "bytestring", + "csv", + "derive_more", + "futures-core", + "futures-util", + "http", + "impl-more", + "itertools", + "local-channel", + "mediatype", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "actix-web-lab-derive" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16294584c7794939b1e5711f28e7cae84ef30e62a520db3f9af425f85269bcd2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "actix-web-utils" version = "0.2.21" @@ -272,6 +344,29 @@ dependencies = [ "libc", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "atoi" version = "1.0.0" @@ -311,15 +406,30 @@ version = "0.1.0" dependencies = [ "actix-cors", "actix-web", + "actix-web-lab", "actix-web-utils", + "bl-types", "chrono", "chrono-tz", - "dotenv", + "dotenvy", + "dotenvy_macro", "err", "serde", "serde_json", "sqlx", "tokio", + "uuid", +] + +[[package]] +name = "bl-types" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-tz", + "serde", + "serde_json", + "uuid", ] [[package]] @@ -404,6 +514,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits", + "serde", "time 0.1.45", "wasm-bindgen", "winapi", @@ -526,6 +637,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "cxx" version = "1.0.94" @@ -614,18 +746,24 @@ dependencies = [ "winapi", ] -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.8.1" @@ -733,6 +871,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -846,6 +985,12 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.8.0" @@ -892,6 +1037,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-more" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2697f323912b5b942f1ff43625c34895edcf3def901c11214ad92d41fa5c57da" + [[package]] name = "indexmap" version = "1.9.3" @@ -1012,6 +1163,12 @@ dependencies = [ "digest", ] +[[package]] +name = "mediatype" +version = "0.19.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea6e62614ab2fc0faa58bb15102a0382d368f896a9fa4776592589ab55c4de7" + [[package]] name = "memchr" version = "2.5.0" @@ -1024,6 +1181,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1411,6 +1578,19 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "serde_html_form" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53192e38d5c88564b924dbe9b60865ecbb71b81d38c4e61c817cffd3e36ef696" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.96" @@ -1806,9 +1986,21 @@ dependencies = [ "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -1824,6 +2016,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1885,6 +2086,23 @@ name = "uuid" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +dependencies = [ + "getrandom", + "rand", + "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d884370cccfad1f913e67c7362f9c00357844bc9f3cfce86faa2241cabd166" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 06c4f2f..d58b2b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,14 @@ chrono-tz = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.88" sqlx = { version = "0.6.0", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", "offline" ] } -dotenv = { version = "0.15.0" } +dotenvy = "0.15.6" +dotenvy_macro = "0.15.1" actix-web = {version = "4.1.0"} actix-cors = "0.6.2" +actix-web-lab = { version = "0.19.1", features = [ "spa" ]} +uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } actix-web-utils = { git = "https://git.franklinblanco.dev/franklinblanco/actix-web-utils.git" } -err = { git = "https://git.franklinblanco.dev/franklinblanco/err.git" } \ No newline at end of file +err = { git = "https://git.franklinblanco.dev/franklinblanco/err.git" } + +bl-types = { path = "../bl-types" } \ No newline at end of file diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..14923ce --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,4 @@ +FROM --platform=linux/amd64 ubuntu +COPY jl-backend / +EXPOSE 8080 +CMD ["./jl-backend"] \ No newline at end of file diff --git a/deploy/deploy-to-backend.sh b/deploy/deploy-to-backend.sh new file mode 100755 index 0000000..1296be1 --- /dev/null +++ b/deploy/deploy-to-backend.sh @@ -0,0 +1,7 @@ +export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc +export CXX_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-g++ +export AR_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-ar +export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-unknown-linux-gnu-gcc +cargo build --release --target x86_64-unknown-linux-gnu +zip -r dist dist/ +scp -i ~/Developer/kp/main-key-pair.pem $CARGO_TARGET_DIR/x86_64-unknown-linux-gnu/release/jl-backend deploy/Dockerfile dist.zip ubuntu@blancolorenzo.pro:~/executables/bl-backend/ \ No newline at end of file diff --git a/migrations/1_contact.sql b/migrations/1_contact.sql new file mode 100644 index 0000000..15d07e5 --- /dev/null +++ b/migrations/1_contact.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS "contact" ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + credential VARCHAR NOT NULL, + message TEXT NOT NULL, + time_created TIMESTAMPTZ NOT NULL +); \ No newline at end of file diff --git a/sql/contact/insert.sql b/sql/contact/insert.sql new file mode 100644 index 0000000..d104ff4 --- /dev/null +++ b/sql/contact/insert.sql @@ -0,0 +1,5 @@ +INSERT INTO contact ( + id, name, credential, message, time_created +) VALUES ( + $1, $2, $3, $4, $5 +) RETURNING *; \ No newline at end of file diff --git a/src/dao/contact.rs b/src/dao/contact.rs new file mode 100644 index 0000000..aa250b9 --- /dev/null +++ b/src/dao/contact.rs @@ -0,0 +1,16 @@ +use bl_types::Contact; +use sqlx::{PgPool}; + +pub async fn insert(db_conn: &PgPool, contact: &Contact) -> Result { + sqlx::query_file_as!( + Contact, + "sql/contact/insert.sql", + contact.id, + contact.name, + contact.credential, + contact.message, + contact.time_created + ) + .fetch_one(db_conn) + .await +} \ No newline at end of file diff --git a/src/dao/mod.rs b/src/dao/mod.rs index e69de29..3c9a93f 100644 --- a/src/dao/mod.rs +++ b/src/dao/mod.rs @@ -0,0 +1,12 @@ +pub mod contact; + +use dotenvy_macro::dotenv; +use sqlx::PgPool; + +pub async fn connect_to_database() -> Result { + let db_url: &str = dotenv!("DATABASE_URL"); + + let formatted_db_url = &format!("{db_url}"); + + sqlx::PgPool::connect(&formatted_db_url).await +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.rs b/src/main.rs index 70992af..7d3f99a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,18 @@ +use std::sync::Arc; + +use chrono::Utc; +use routes::start_all_routes; + mod services; mod routes; -mod domain; mod dao; - +pub mod utils; #[tokio::main] async fn main() { + let start_time = Utc::now().timestamp_millis(); + let db_conn = Arc::new(dao::connect_to_database().await.unwrap()); + + start_all_routes(start_time, db_conn).await.expect("Something happened..."); } \ No newline at end of file diff --git a/src/routes/contact.rs b/src/routes/contact.rs new file mode 100644 index 0000000..5b6fa58 --- /dev/null +++ b/src/routes/contact.rs @@ -0,0 +1,16 @@ +use std::sync::Arc; + +use actix_web::{post, web}; +use actix_web_utils::extensions::typed_response::TypedHttpResponse; +use bl_types::NewContactPayload; +use sqlx::PgPool; + +use crate::services; + +#[post("contact")] +pub async fn add_new_contact( + db_conn: web::Data>, + contact: web::Json, +) -> TypedHttpResponse<()> { + services::contact::add_new_contact(&db_conn, contact.0).await +} \ No newline at end of file diff --git a/src/routes/mod.rs b/src/routes/mod.rs index e69de29..f1f5819 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -0,0 +1,50 @@ +pub mod contact; + +use std::{sync::Arc}; + +use actix_cors::Cors; +use actix_web::{ + web::{self}, + App, HttpServer, +}; +use actix_web_lab::web::spa; +use chrono::Utc; +use sqlx::PgPool; + +use self::contact::add_new_contact; + +pub const HOST_ADDR: &str = "0.0.0.0"; +pub const HOST_PORT: u16 = 8085; + +pub async fn start_all_routes(start_time: i64, db_conn: Arc) -> Result<(), std::io::Error> { + // Start server code that turns into a future to be executed below + let server_future = HttpServer::new(move || { + let cors_policy = Cors::permissive(); + App::new() + .wrap(cors_policy) + .app_data(web::Data::new(db_conn.clone())) + .service( + web::scope("/api") + .service(add_new_contact) + ) + .service( + spa() + .index_file("./dist/index.html") + .static_resources_mount("/") + .static_resources_location("./dist") + .finish() + ) + }) + .bind((HOST_ADDR, HOST_PORT))? + .run(); + + after_startup_fn(start_time); + server_future.await +} + +pub fn after_startup_fn(start_time: i64) { + println!( + "Server took {}ms to setup", + Utc::now().timestamp_millis() - start_time + ); +} diff --git a/src/services/contact.rs b/src/services/contact.rs new file mode 100644 index 0000000..7e2b99d --- /dev/null +++ b/src/services/contact.rs @@ -0,0 +1,17 @@ +use actix_web_utils::extensions::typed_response::TypedHttpResponse; +use bl_types::NewContactPayload; +use sqlx::PgPool; + +use crate::{handle_db_write_op, dao, handle_tx, success}; + + + +pub async fn add_new_contact( + db_conn: &PgPool, + contact: NewContactPayload, +) -> TypedHttpResponse<()> { + let tx = handle_tx!(db_conn.begin()); + handle_db_write_op!(dao::contact::insert(db_conn, &contact.into()), tx); + handle_tx!(tx.commit()); + success!(()) +} \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs index e69de29..9fd92c1 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -0,0 +1 @@ +pub mod contact; \ No newline at end of file diff --git a/src/utils/macros.rs b/src/utils/macros.rs new file mode 100644 index 0000000..ee0443b --- /dev/null +++ b/src/utils/macros.rs @@ -0,0 +1,74 @@ +/// This macro unwraps the value and if its an error it rolls back the transaction and returns a TypedHttpResponse with the corresponding erorr. +#[macro_export] +macro_rules! handle_db_write_op { + ($e:expr, $tx:expr) => { + match $e.await { + Ok(value) => value, + Err(error) => { + handle_tx!($tx.rollback()); + return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(400, err::MessageResource::new_from_string(error.to_string())); + } + } + }; +} + +#[macro_export] +macro_rules! handle_db_read_op { + ($e:expr) => { + match $e.await { + Ok(value) => value, + Err(error) => return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(400, err::MessageResource::new_from_string(error.to_string())), + } + }; +} + +/// This macro calls await on whatever you give it and if it gets an error it returns a TypedHttpResponse with an InternalServerError status code (500) and an error message. +#[macro_export] +macro_rules! handle_tx { + ($e:expr) => { + match $e.await { + Ok(value) => value, + Err(_) => { + return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(500, err::MessageResource::new_from_str("Failed to acquire, commit or rollback tx...")); + } + } + }; +} + +/// This macro just returns a TypedHttpResponse with a success status code (200) and whatever you give it inside. +#[macro_export] +macro_rules! success { + ($e:expr) => { + return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_response(200, $e) + }; +} + +/// This macro just returns a TypedHttpResponse with a not found status code (404) and an error concatenated. +/// The literal should be a subject in plural form: +/// Agent -> agents +#[macro_export] +macro_rules! unwrap_or_not_found { + ($e:expr, $what:literal) => { + match $e { + Some(value) => value, + None => return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(404, err::MessageResource::new_from_string(format!("No {} found with specified Id.", $what))), + } + }; +} + +/// This macro does the authentication needed for all the admin routes. give it a user and password as params. +#[macro_export] +macro_rules! auth { + ($request:expr) => { + let __auth_token_header = match $request.headers().get("auth_token") { + Some(header_val) => {header_val.to_str().expect("ASCII Chars")}, + None => {return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(401, err::MessageResource::new_from_string(format!("No Token in header.")))} + }; + match crate::utils::auth::get_token() { + Some(token) => if token == __auth_token_header {} else { + return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(401, err::MessageResource::new_from_string(format!("Incorrect Token."))) + }, + None => return actix_web_utils::extensions::typed_response::TypedHttpResponse::return_standard_error(401, err::MessageResource::new_from_string(format!("No Token in backend. Please authenticate."))), + } + }; +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..d28af63 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod macros; \ No newline at end of file