From 861c6731c7b4a17340b30f0d9c23ac7c2473aa06 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sun, 10 Mar 2024 13:02:25 -0700 Subject: [PATCH] Add receipt to DB to prevent double-use. --- Cargo.lock | 4 +- .../20240308005811_users_invitations.down.sql | 4 +- .../20240308005811_users_invitations.up.sql | 4 +- src/handlers/handlers.rs | 69 ++++++++++++++----- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cffec90..26b1748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,9 +2018,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ "redox_syscall", "wasite", diff --git a/migrations/20240308005811_users_invitations.down.sql b/migrations/20240308005811_users_invitations.down.sql index ec254e3..586cef2 100644 --- a/migrations/20240308005811_users_invitations.down.sql +++ b/migrations/20240308005811_users_invitations.down.sql @@ -2,9 +2,9 @@ drop table if exists customers; drop index if exists customers_username_dex; drop index if exists customers_email_dex; drop index if exists customers_invitation_dex; -drop trigger if exists update_last_updated_customers; +drop trigger if exists update_customers_updated_at; drop table if exists invitations; drop index if exists invitations_owner_dex; -drop trigger if exists update_updated_at_invitations; +drop trigger if exists update_invitations_updated_at; diff --git a/migrations/20240308005811_users_invitations.up.sql b/migrations/20240308005811_users_invitations.up.sql index 1cff5b9..3535be3 100644 --- a/migrations/20240308005811_users_invitations.up.sql +++ b/migrations/20240308005811_users_invitations.up.sql @@ -13,7 +13,7 @@ create index if not exists customers_email_dex on customers (lower(billing_email create index if not exists customers_receipt_dex on customers (receipt); create index if not exists customers_invitation_dex on customers (invitation); -- does this need to be created? it's already a foreign key -create trigger if not exists update_last_updated_customers +create trigger if not exists update_customers_updated_at after update on customers when OLD.updated_at = NEW.updated_at or OLD.updated_at is null BEGIN @@ -32,7 +32,7 @@ create table if not exists invitations ( ); create index if not exists invitations_owner_dex on invitations (owner); -create trigger if not exists update_updated_at_invitations +create trigger if not exists update_invitations_updated_at after update on invitations when OLD.updated_at = NEW.updated_at or OLD.updated_at is null BEGIN diff --git a/src/handlers/handlers.rs b/src/handlers/handlers.rs index 3480c97..ad253e9 100644 --- a/src/handlers/handlers.rs +++ b/src/handlers/handlers.rs @@ -27,7 +27,11 @@ lazy_static! { static ref MONTHLY_LINK: String = std::env::var("MONTHLY_LINK").unwrap(); } -/// Displays the signup form. +//-************************************************************************ +// handlers: get_signup, post_signup, and payment_success +//-************************************************************************ + +/// Displays the signup page with links to Stripe pub async fn get_signup() -> impl IntoResponse { SignupPage { monthly_link: Some((*MONTHLY_LINK).to_string()), @@ -40,36 +44,55 @@ pub async fn post_signup( State(db): State, Form(form): Form, ) -> Result { + let receipt = form.receipt.trim(); + if confirm_payment(&db, receipt).await { + log::info!("Confirmed payment again from {receipt}"); + } else { + log::warn!("Attempt to use duplicate receipt {receipt}"); + return Err(CreateUserError(CreateUserErrorKind::BadPayment)); + } + let user = validate_signup(&form).await?; if create_user(&user) { log::info!("Created user {user:?}"); - insert_user(&db, &form.username, form.receipt.trim(), None).await; + insert_customer(&db, &form.username, form.receipt.trim(), None).await; Ok(SignupSuccessPage(user)) } else { Err(CreateUserError(CreateUserErrorKind::UnknownEorr)) } } -/// Redirected from Stripe with the receipt of payment. -pub async fn payment_success(receipt: Option>) -> impl IntoResponse { +/// Redirected from Stripe with the receipt of payment, and shows the signup +/// form for creating your account. +pub async fn payment_success( + State(db): State, + receipt: Option>, +) -> impl IntoResponse { let receipt = if let Some(Path(receipt)) = receipt { receipt } else { return CreateUserError(CreateUserErrorKind::BadPayment).into_response(); }; + let receipt = receipt.trim(); + if confirm_payment(&db, receipt).await { + log::info!("Confirmed payment from {receipt}"); + } else { + log::warn!("Attempt to use duplicate receipt {receipt}"); + return CreateUserError(CreateUserErrorKind::BadPayment).into_response(); + } UserFormPage { - receipt, + receipt: receipt.to_string(), ..Default::default() } .into_response() } //-************************************************************************ -// helpers +// private helpers for the handlers //-************************************************************************ -async fn insert_user(db: &SqlitePool, username: &str, receipt: &str, invitation: Option<&str>) { - sqlx::query!( +async fn insert_customer(db: &SqlitePool, username: &str, receipt: &str, invitation: Option<&str>) { + match sqlx::query!( "insert into customers (username, receipt, invitation) values (?, ?, ?)", username, receipt, @@ -77,7 +100,12 @@ async fn insert_user(db: &SqlitePool, username: &str, receipt: &str, invitation: ) .execute(db) .await - .unwrap_or_default(); + { + Ok(_) => {} + Err(e) => { + log::error!("Could not insert {receipt} for {username} into DB, got {e}"); + } + } } fn create_user(user: &User) -> bool { @@ -100,7 +128,21 @@ fn create_user(user: &User) -> bool { } } -fn confirm_payment(stripe_checkout_session_id: &str) -> bool { +async fn confirm_payment(db: &SqlitePool, stripe_checkout_session_id: &str) -> bool { + // first check the receipt to see that it hasn't been used already + match sqlx::query_scalar!( + "select count(*) from customers where receipt = ?", + stripe_checkout_session_id + ) + .fetch_one(db) + .await + .unwrap_or(0) + { + 0 => {} + _ => return false, + } + + // ok see if Stripe knows about it let token = &*STRIPE_TOKEN; let url = format!("https://api.stripe.com/v1/checkout/sessions/{stripe_checkout_session_id}"); let json: serde_json::Value = ureq::get(&url) @@ -130,13 +172,6 @@ async fn validate_signup(form: &SignupForm) -> Result { let username = form.username.trim(); let password = form.password.trim(); let verify = form.pw_verify.trim(); - let receipt = form.receipt.trim(); - - if confirm_payment(receipt) { - log::info!("Confirmed payment from {receipt}"); - } else { - return Err(CreateUserError(CreateUserErrorKind::BadPayment)); - } let name_len = username.graphemes(true).size_hint().1.unwrap_or(0); // we are not ascii exclusivists around here