From 5596724ba9b7b605807c839928aadc65ad0818c9 Mon Sep 17 00:00:00 2001 From: "Z. Charles Dziura" Date: Thu, 22 Aug 2024 17:29:24 -0400 Subject: [PATCH] Beginning to rip apart the API --- api/.env | 8 +- api/Cargo.toml | 27 +++++- api/src/db/user.rs | 84 +++++++++---------- api/src/main.rs | 10 +-- api/src/models/environment.rs | 69 +++++++++++++-- api/src/requests/mod.rs | 10 ++- api/src/requests/user/mod.rs | 28 +++++-- api/src/requests/user/new_user/models/mod.rs | 8 +- ...ost_request.rs => registration_request.rs} | 7 +- ...t_response.rs => registration_response.rs} | 4 +- api/src/requests/user/new_user/request.rs | 72 +++++++--------- 11 files changed, 205 insertions(+), 122 deletions(-) rename api/src/requests/user/new_user/models/{post_request.rs => registration_request.rs} (55%) rename api/src/requests/user/new_user/models/{post_response.rs => registration_response.rs} (89%) diff --git a/api/.env b/api/.env index 187329b..840fd28 100644 --- a/api/.env +++ b/api/.env @@ -1,4 +1,8 @@ +HOSTNAME=localhost +PORT=42068 +DOMAIN=http://localhost:42069 +RP_ID=localhost TOKEN_KEY=k4.local.hWoS2ZulK9xPEATtXH1Dvj_iynzqfUv5ER5_IFTg5-Q -DATABASE_URL=postgres://debt-pirate:test@192.168.122.241/debt_pirate +DATABASE_URL=postgres://debt-pirate:test@192.168.122.215/debt_pirate ASSETS_DIR=/home/zcdziura/Documents/Projects/debt-pirate/api/assets -MAINTENANCE_USER_ACCOUNT=dreadnought_maintenance:HRURqlUmtjIy +MAINTENANCE_USER_ACCOUNT=debt_pirate:HRURqlUmtjIy diff --git a/api/Cargo.toml b/api/Cargo.toml index 9795e2d..1668b76 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -5,22 +5,41 @@ edition = "2021" [dependencies] argon2 = "0.5" -axum = { version = "0.7", features = ["default", "macros", "multipart", "ws"], default-features = false } +axum = { version = "0.7", features = [ + "default", + "macros", + "multipart", + "ws", +], default-features = false } base64 = "0.22" dotenvy = "0.15" futures = "0.3" http = "1.0.0" humantime = "2.1.0" hyper = { version = "1.1", features = ["full"] } -lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "tokio1", "tokio1-rustls-tls"] } +lettre = { version = "0.11", default-features = false, features = [ + "builder", + "hostname", + "pool", + "smtp-transport", + "tokio1", + "tokio1-rustls-tls", +] } log = "0.4" num_cpus = "1.16" once_cell = "1.19" pasetors = "0.6" serde = { version = "1.0", features = ["derive", "rc", "std"] } serde_json = "1.0" -sqlx = { version = "0.7", features = ["default", "chrono", "postgres", "runtime-tokio"] } +sqlx = { version = "0.8", features = [ + "default", + "chrono", + "postgres", + "runtime-tokio", +] } tokio = { version = "1.35", features = ["full"] } tower = "0.4" tower-http = { version = "0.5", features = ["full"] } -uuid = { version = "1.8.0", features = ["v7", "fast-rng"] } +tower-sessions = "0.12.3" +uuid = { version = "1.10", features = ["serde", "v7"] } +webauthn-rs = { version = "0.5.0", features = ["danger-allow-state-serialisation"] } diff --git a/api/src/db/user.rs b/api/src/db/user.rs index ebe160e..70e0652 100644 --- a/api/src/db/user.rs +++ b/api/src/db/user.rs @@ -1,5 +1,5 @@ -use argon2::password_hash::PasswordHashString; use sqlx::prelude::FromRow; +use uuid::Uuid; use crate::error::AppError; @@ -7,24 +7,17 @@ use super::DbPool; #[derive(Debug)] pub struct NewUserEntity { - pub username: String, pub email: String, - pub password: String, pub name: String, + pub user_id: Uuid, } impl NewUserEntity { - pub fn new( - username: String, - email: String, - password: PasswordHashString, - name: String, - ) -> Self { + pub fn new(name: String, email: String) -> Self { Self { - username, - email, - password: password.as_str().to_owned(), name, + email, + user_id: Uuid::now_v7(), } } } @@ -33,43 +26,42 @@ impl NewUserEntity { #[derive(Debug, FromRow)] pub struct UserEntity { pub id: i32, - pub username: String, - pub email: String, pub name: String, + pub email: String, + pub user_id: Uuid, pub status_id: i32, } -pub async fn insert_new_user( - pool: &DbPool, - new_user: NewUserEntity, -) -> Result { - let NewUserEntity { - username, - email, - password, - name, - } = new_user; +// pub async fn _insert_new_user( +// pool: &DbPool, +// new_user: NewUserEntity, +// ) -> Result { +// let NewUserEntity { +// name, +// email, +// user_id, +// } = new_user; - sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;") - .bind(username) - .bind(email) - .bind(password) - .bind(name) - .fetch_one(pool).await - .map_err(|err| { - eprintln!("Error inserting NewUserEntity {err:?}"); - AppError::from(err) - }) -} +// sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (username, email, password, name) VALUES ($1, $2, $3, $4) RETURNING id, username, email, name, status_id;") +// .bind(username) +// .bind(email) +// .bind(password) +// .bind(name) +// .fetch_one(pool).await +// .map_err(|err| { +// eprintln!("Error inserting NewUserEntity {err:?}"); +// AppError::from(err) +// }) +// } -pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> { - sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;") - .bind(user_id) - .execute(pool) - .await - .map_err(|err| { - eprintln!("Error verifying user with id '{user_id}'."); - AppError::from(err) - }) - .map(|_| ()) -} +// pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> { +// sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;") +// .bind(user_id) +// .execute(pool) +// .await +// .map_err(|err| { +// eprintln!("Error verifying user with id '{user_id}'."); +// AppError::from(err) +// }) +// .map(|_| ()) +// } diff --git a/api/src/main.rs b/api/src/main.rs index 0ccc58d..24dba1b 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -8,7 +8,7 @@ mod models; mod requests; mod services; -use db::{create_connection_pool, run_migrations}; +use db::create_connection_pool; use requests::start_app; use services::{start_emailer_service, UserConfirmationMessage}; use tokio::runtime::Handle; @@ -26,10 +26,10 @@ async fn main() { }; let pool = create_connection_pool(env.db_connection_uri()).await; - if let Err(err) = run_migrations(&pool).await { - eprintln!("{err:?}"); - process::exit(2); - } + // if let Err(err) = run_migrations(&pool).await { + // eprintln!("{err:?}"); + // process::exit(2); + // } start_emailer_service(Handle::current(), env.assets_dir(), rx); diff --git a/api/src/models/environment.rs b/api/src/models/environment.rs index 30fa27e..5034ff0 100644 --- a/api/src/models/environment.rs +++ b/api/src/models/environment.rs @@ -19,6 +19,10 @@ static REQUIRED_ENV_VARS: Lazy> = Lazy::new(|| { #[derive(Clone)] pub struct Environment { + hostname: String, + port: u32, + domain: String, + rp_id: String, token_key: SymmetricKey, database_url: String, email_sender: Sender, @@ -33,19 +37,34 @@ impl Environment { .filter_map(|item| item.ok()) .filter(|(key, _)| REQUIRED_ENV_VARS.contains(key)) .for_each(|(key, value)| match key.as_str() { + "HOSTNAME" => builder.with_hostname(value), + "PORT" => builder.with_port(value), "TOKEN_KEY" => builder.with_token_key(value), "DATABASE_URL" => builder.with_database_url(value), "ASSETS_DIR" => builder.with_assets_dir(value), _ => {} }); - if let Some(missing_vars) = builder.uninitialized_variables() { + let missing_vars = builder.uninitialized_variables(); + if let Some(missing_vars) = missing_vars { Err(AppError::missing_environment_variables(missing_vars)) } else { Ok(Environment::from(builder)) } } + pub fn hostname(&self) -> &str { + self.hostname.as_str() + } + + pub fn port(&self) -> u32 { + self.port + } + + pub fn domain(&self) -> &str { + self.domain.as_str() + } + pub fn token_key(&self) -> &SymmetricKey { &self.token_key } @@ -66,6 +85,10 @@ impl Environment { impl From for Environment { fn from(builder: EnvironmentObjectBuilder) -> Self { let EnvironmentObjectBuilder { + hostname, + port, + domain, + rp_id, token_key, database_url, email_sender, @@ -73,6 +96,10 @@ impl From for Environment { } = builder; Self { + hostname: hostname.unwrap(), + port: port.unwrap(), + domain: domain.unwrap(), + rp_id: rp_id.unwrap(), token_key: token_key.unwrap(), database_url: database_url.unwrap(), email_sender: email_sender.unwrap(), @@ -83,6 +110,10 @@ impl From for Environment { #[derive(Default)] pub struct EnvironmentObjectBuilder { + pub hostname: Option, + pub port: Option, + pub domain: Option, + pub rp_id: Option, pub token_key: Option>, pub database_url: Option, pub email_sender: Option>, @@ -98,15 +129,20 @@ impl EnvironmentObjectBuilder { } pub fn uninitialized_variables(&self) -> Option> { - let mut missing_vars = Vec::with_capacity(3); + let mut missing_vars = [ + ("HOSTNAME", self.hostname.as_deref()), + ("DOMAIN", self.domain.as_deref()), + ("RP_ID", self.rp_id.as_deref()), + ("DATABASE_URL", self.database_url.as_deref()), + ] + .into_iter() + .filter_map(|(key, value)| value.xor(Some(key))) + .collect::>(); + if self.token_key.is_none() { missing_vars.push("TOKEN_KEY"); } - if self.database_url.is_none() { - missing_vars.push("DATABASE_URL"); - } - if self.assets_dir.is_none() { missing_vars.push("ASSETS_DIR"); } @@ -118,6 +154,27 @@ impl EnvironmentObjectBuilder { } } + pub fn with_hostname(&mut self, hostname: String) { + self.hostname = Some(hostname); + } + + pub fn with_port(&mut self, port: String) { + let port = port + .parse::() + .inspect_err(|err| eprintln!("Not a valid port, defaulting to '42069': {err}")) + .ok(); + + self.port = port; + } + + pub fn with_domain(&mut self, domain: String) { + self.domain = Some(domain); + } + + pub fn with_rp_id(&mut self, rp_id: String) { + self.rp_id = Some(rp_id); + } + pub fn with_token_key(&mut self, key: String) { match SymmetricKey::::try_from(key.as_str()).map_err(|_| AppError::token_key()) { Ok(key) => self.token_key = Some(key), diff --git a/api/src/requests/mod.rs b/api/src/requests/mod.rs index b5cb126..e4bee0c 100644 --- a/api/src/requests/mod.rs +++ b/api/src/requests/mod.rs @@ -1,7 +1,10 @@ mod user; +use std::sync::Arc; + use axum::Router; use tokio::net::TcpListener; +use webauthn_rs::Webauthn; use crate::{db::DbPool, error::AppError, models::Environment}; @@ -9,10 +12,13 @@ use crate::{db::DbPool, error::AppError, models::Environment}; pub struct AppState { pool: DbPool, env: Environment, + webauthn: Arc, } impl AppState { pub fn new(pool: DbPool, env: Environment) -> Self { + let rp_id = + Self { pool, env } } @@ -26,8 +32,8 @@ impl AppState { } pub async fn start_app(pool: DbPool, env: Environment) -> Result<(), AppError> { - let address = "localhost"; - let port = "42069"; + let address = env.hostname(); + let port = env.port(); let listener = TcpListener::bind(format!("{address}:{port}")) .await .unwrap(); diff --git a/api/src/requests/user/mod.rs b/api/src/requests/user/mod.rs index bee043d..c920847 100644 --- a/api/src/requests/user/mod.rs +++ b/api/src/requests/user/mod.rs @@ -1,15 +1,29 @@ use axum::Router; +use tower::ServiceBuilder; +use tower_sessions::{ + cookie::{time::Duration, SameSite}, + Expiry, MemoryStore, SessionManagerLayer, +}; use super::AppState; pub mod new_user; -pub mod verify; +// pub mod verify; pub fn requests(app_state: AppState) -> Router { - Router::new().nest( - "/user", - Router::new() - .merge(new_user::request(app_state.clone())) - .merge(verify::request(app_state.clone())), - ) + let domain = app_state.env().domain().to_owned(); + let user_requests_middleware = ServiceBuilder::new().layer( + SessionManagerLayer::new(MemoryStore::default()) + .with_domain(domain) + .with_secure(false) + .with_same_site(SameSite::Strict) + .with_expiry(Expiry::OnInactivity(Duration::seconds(300))), + ); + + Router::new() + .nest( + "/user", + Router::new().merge(new_user::request(app_state.clone())), // .merge(verify::request(app_state.clone())), + ) + .layer(user_requests_middleware) } diff --git a/api/src/requests/user/new_user/models/mod.rs b/api/src/requests/user/new_user/models/mod.rs index c547d51..d30e4ef 100644 --- a/api/src/requests/user/new_user/models/mod.rs +++ b/api/src/requests/user/new_user/models/mod.rs @@ -1,5 +1,5 @@ -mod post_request; -mod post_response; +mod registration_request; +mod registration_response; -pub use post_request::*; -pub use post_response::*; +pub use registration_request::*; +pub use registration_response::*; diff --git a/api/src/requests/user/new_user/models/post_request.rs b/api/src/requests/user/new_user/models/registration_request.rs similarity index 55% rename from api/src/requests/user/new_user/models/post_request.rs rename to api/src/requests/user/new_user/models/registration_request.rs index 6a9b934..259506b 100644 --- a/api/src/requests/user/new_user/models/post_request.rs +++ b/api/src/requests/user/new_user/models/registration_request.rs @@ -1,9 +1,8 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] -pub struct UserPostRequest { - pub username: String, - pub password: String, - pub email: String, +#[serde(rename_all = "camelCase")] +pub struct UserRegistrationRequest { pub name: String, + pub email: String, } diff --git a/api/src/requests/user/new_user/models/post_response.rs b/api/src/requests/user/new_user/models/registration_response.rs similarity index 89% rename from api/src/requests/user/new_user/models/post_response.rs rename to api/src/requests/user/new_user/models/registration_response.rs index 91f5968..970f0cf 100644 --- a/api/src/requests/user/new_user/models/post_response.rs +++ b/api/src/requests/user/new_user/models/registration_response.rs @@ -5,12 +5,12 @@ use serde::Serialize; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct UserPostResponse { +pub struct UserRegistrationResponse { user_id: i32, auth: UserPostResponseToken, } -impl UserPostResponse { +impl UserRegistrationResponse { pub fn new(user_id: i32, token: String, expiration: SystemTime) -> Self { let auth = UserPostResponseToken { token, diff --git a/api/src/requests/user/new_user/request.rs b/api/src/requests/user/new_user/request.rs index 7270291..1039841 100644 --- a/api/src/requests/user/new_user/request.rs +++ b/api/src/requests/user/new_user/request.rs @@ -7,69 +7,61 @@ use axum::{ Json, Router, }; use http::StatusCode; +use tower_sessions::Session; use crate::{ - db::{insert_new_user, NewUserEntity, UserEntity}, + db::NewUserEntity, models::ApiResponse, requests::AppState, - services::{auth_token::generate_token, hash_string, UserConfirmationMessage}, + services::{auth_token::generate_token, UserConfirmationMessage}, }; -use super::models::{UserPostRequest, UserPostResponse}; +use super::models::{UserRegistrationRequest, UserRegistrationResponse}; static FIFTEEN_MINUTES: u64 = 60 * 15; pub fn request(app_state: AppState) -> Router { Router::new() - .route("/", post(user_post_handler)) + .route("/", post(user_registration_post_handler)) .with_state(app_state) } -async fn user_post_handler( +async fn user_registration_post_handler( State(app_state): State, - Json(request): Json, + session: Session, + Json(request): Json, ) -> Result { - let UserPostRequest { - username, - password, - email, - name, - } = request; + session.remove_value("reg_state"); - let hashed_password = hash_string(password.as_str()); - let new_user = NewUserEntity::new(username, email.clone(), hashed_password, name.clone()); - let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await - .map_err(|err| { - if err.is_duplicate_record() { - (StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response() - } else { - (StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response() - } - })?; + let UserRegistrationRequest { name, email } = request; + // let new_user = NewUserEntity::new(email.clone(), name.clone()); + // let UserEntity { id: user_id, name, email , ..} = insert_new_user(app_state.pool(), new_user).await + // .map_err(|err| { + // if err.is_duplicate_record() { + // (StatusCode::CONFLICT, ApiResponse::error("There is already an account associated with this username or email address.").into_json_response()).into_response() + // } else { + // (StatusCode::INTERNAL_SERVER_ERROR, ApiResponse::error("An error occurred while creating your new user account. Please try again later.").into_json_response()).into_response() + // } + // })?; - let (auth_token, expiration) = generate_token( - app_state.env().token_key(), - user_id, - Some(Duration::from_secs(FIFTEEN_MINUTES)), - Some(format!("/user/{user_id}/verify").as_str()), - ); + // let new_user_confirmation_message = + // UserConfirmationMessage::new(email.as_str(), name.as_str(), auth_token.as_str()); - let new_user_confirmation_message = - UserConfirmationMessage::new(email.as_str(), name.as_str(), auth_token.as_str()); + // let _ = app_state + // .env() + // .email_sender() + // .send(new_user_confirmation_message) + // .inspect_err(|err| { + // eprintln!("Got the rollowing error while sending across the channel: {err}"); + // }); - let _ = app_state - .env() - .email_sender() - .send(new_user_confirmation_message) - .inspect_err(|err| { - eprintln!("Got the rollowing error while sending across the channel: {err}"); - }); + let new_user_entity = NewUserEntity::new(name, email); let response = ( StatusCode::CREATED, - Json(ApiResponse::::new(UserPostResponse::new( - user_id, auth_token, expiration, - ))), + Json(ApiResponse::::new( + UserRegistrationResponse::new(user_id, auth_token, expiration), + )), ); Ok(response.into_response())