Просмотр исходного кода

Create route to handle stripe webhooks

Lee Morgan 1 месяц назад
Родитель
Сommit
815674ff43

Разница между файлами не показана из-за своего большого размера
+ 397 - 60
Cargo.lock


+ 1 - 0
Cargo.toml

@@ -7,6 +7,7 @@ edition = "2024"
 actix-cors = "0.7.1"
 actix-web = "4.13.0"
 argon2 = "0.5.3"
+async-stripe = { version = "0.41.0", features = ["runtime-tokio-hyper", "webhook-events"] }
 chrono = { version = "0.4.44", features = ["serde"]}
 dotenvy = "0.15.7"
 jsonwebtoken = "9"

+ 1 - 0
src/controllers/mod.rs

@@ -1 +1,2 @@
 pub mod user;
+pub mod webhook;

+ 1 - 0
src/controllers/webhook/mod.rs

@@ -0,0 +1 @@
+pub mod stripe;

+ 59 - 0
src/controllers/webhook/stripe.rs

@@ -0,0 +1,59 @@
+use actix_web::{HttpResponse, HttpRequest, web, post};
+use stripe::{Event, EventType, EventObject, Webhook};
+use sqlx::PgPool;
+use crate::{
+    http_error::HttpError,
+    models::purchase::{Purchase, Status},
+    STRIPE_WEBHOOK_SECRET
+};
+
+#[post("/webhook/stripe")]
+pub async fn route(
+    req: HttpRequest,
+    db: web::Data<PgPool>,
+    body: web::Bytes
+) -> Result<HttpResponse, HttpError> {
+    let payload = std::str::from_utf8(&body).unwrap();
+    let signature = get_signature(&req)?;
+    let secret = STRIPE_WEBHOOK_SECRET.get().unwrap();
+    let event = Webhook::construct_event(payload, &signature, secret)
+        .map_err(|_| HttpError::InvalidInput("Failed to verify webhook".into()))?;
+
+    match event.type_ {
+        EventType::PaymentIntentSucceeded => {handle_success(event, &db).await?},
+        EventType::PaymentIntentPaymentFailed => {handle_failure(event, &db).await?},
+        _ => ()
+    };
+
+    Ok(HttpResponse::Ok().finish())
+}
+
+fn get_signature(req: &HttpRequest) -> Result<String, HttpError> {
+    let signature = req.headers()
+        .get("stripe-signature")
+        .and_then(|v| v.to_str().ok())
+        .ok_or_else(|| HttpError::InvalidInput("No signature".into()))?
+        .to_string();
+
+    Ok(signature)
+}
+
+async fn handle_success(event: Event, db: &PgPool) -> Result<(), HttpError> {
+    let id = match event.data.object {
+        EventObject::PaymentIntent(pi) => pi.id.to_string(),
+        _ => {return Err(HttpError::InvalidInput("No payment intent ID".into()))}
+    };
+
+    Purchase::update_status(db, id, Status::Succeeded).await?;
+    Ok(())
+}
+
+async fn handle_failure(event: Event, db: &PgPool) -> Result<(), HttpError> {
+    let id = match event.data.object {
+        EventObject::PaymentIntent(pi) => pi.id.to_string(),
+        _ => {return Err(HttpError::InvalidInput("No payment intent ID".into()))}
+    };
+
+    Purchase::update_status(db, id, Status::Succeeded).await?;
+    Ok(())
+}

+ 3 - 0
src/main.rs

@@ -15,6 +15,7 @@ static JWT_SECRET: OnceLock<String> = OnceLock::new();
 static STRIPE_KEY: OnceLock<String> = OnceLock::new();
 static TOKEN_PACK_PRICE: OnceLock<i32> = OnceLock::new();
 static TOKENS_PER_PACK: OnceLock<i32> = OnceLock::new();
+static STRIPE_WEBHOOK_SECRET: OnceLock<String> = OnceLock::new();
 
 #[actix_web::main]
 async fn main() -> std::io::Result<()> {
@@ -28,6 +29,7 @@ async fn main() -> std::io::Result<()> {
     STRIPE_KEY.set(env_var("STRIPE_KEY")).expect("Failed to set Stripe key");
     TOKEN_PACK_PRICE.set(env_var("TOKEN_PACK_PRICE")).expect("Failed to set token pack price");
     TOKENS_PER_PACK.set(env_var("TOKENS_PER_PACK")).expect("Failed to set tokens per pack");
+    STRIPE_WEBHOOK_SECRET.set(env_var("STRIPE_WEBHOOK_SECRET")).expect("Failed to set stripe webhook secret");
 
     //Database
     let pool = PgPool::connect(&database_url)
@@ -62,6 +64,7 @@ async fn main() -> std::io::Result<()> {
             )
             .app_data(web::Data::new(pool.clone()))
             .configure(routes::user::config)
+            .configure(routes::webhook::config)
     })
         .bind(("0.0.0.0", port))
         .expect("Failed to bind to port")

+ 14 - 0
src/models/purchase.rs

@@ -97,4 +97,18 @@ impl Purchase {
 
         Ok(())
     }
+
+    pub async fn update_status(db: &PgPool, stripe_id: String, status: Status) -> Result<(), HttpError> {
+        sqlx::query(
+            "UPDATE purchases
+            SET status = $2
+            WHERE stripe_id = $1"
+        )
+            .bind(stripe_id)
+            .bind(status.to_string())
+            .execute(db)
+            .await?;
+
+        Ok(())
+    }
 }

+ 1 - 0
src/routes/mod.rs

@@ -1 +1,2 @@
 pub mod user;
+pub mod webhook;

+ 8 - 0
src/routes/webhook.rs

@@ -0,0 +1,8 @@
+use actix_web::web;
+use crate::controllers::webhook::{
+    stripe
+};
+
+pub fn config(cfg: &mut web::ServiceConfig) {
+    cfg.service(stripe::route);
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов