Files
crawlapi/crates/api/src/routes/stripe.rs
Developer 62994d4f3d
Some checks failed
CI / Test (push) Has been cancelled
Deploy / Deploy to Staging (push) Has been cancelled
CI / Build & Push (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Initial commit: Full Crawl API implementation
2026-04-29 07:03:48 +00:00

147 lines
5.1 KiB
Rust

use axum::{
extract::State,
http::{HeaderMap, StatusCode},
Json,
};
use db::repos::{subscriptions, users};
use serde::{Deserialize, Serialize};
use crate::{middleware::jwt::JwtAuth, state::AppState};
use axum::Extension;
#[derive(Debug, Deserialize)]
pub struct CreateCheckoutRequest {
pub price_id: String,
}
#[derive(Debug, Serialize)]
pub struct CheckoutResponse {
pub checkout_url: String,
}
pub async fn create_checkout(
State(state): State<AppState>,
Extension(auth): Extension<JwtAuth>,
Json(body): Json<CreateCheckoutRequest>,
) -> Result<Json<CheckoutResponse>, StatusCode> {
let stripe_secret = std::env::var("STRIPE_SECRET_KEY").map_err(|_| StatusCode::NOT_IMPLEMENTED)?;
let user = users::find_by_id(&state.db, auth.user_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
// Create Stripe customer via HTTP API directly (simpler than SDK for MVP)
let client = reqwest::Client::new();
let customer_res = client
.post("https://api.stripe.com/v1/customers")
.basic_auth(&stripe_secret, Some(""))
.form(&[("email", &user.email)])
.send()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let customer_data: serde_json::Value = customer_res.json().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let customer_id = customer_data["id"].as_str().unwrap_or("");
let success_url = std::env::var("STRIPE_SUCCESS_URL")
.unwrap_or_else(|_| "http://localhost:3000/dashboard?success=true".to_string());
let cancel_url = std::env::var("STRIPE_CANCEL_URL")
.unwrap_or_else(|_| "http://localhost:3000/dashboard?canceled=true".to_string());
let session_res = client
.post("https://api.stripe.com/v1/checkout/sessions")
.basic_auth(&stripe_secret, Some(""))
.form(&[
("customer", customer_id),
("success_url", &success_url),
("cancel_url", &cancel_url),
("mode", "subscription"),
("line_items[0][price]", &body.price_id),
("line_items[0][quantity]", "1"),
("metadata[user_id]", &auth.user_id.to_string()),
])
.send()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let session_data: serde_json::Value = session_res.json().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let url = session_data["url"].as_str().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(CheckoutResponse { checkout_url: url.to_string() }))
}
#[derive(Debug, Deserialize)]
pub struct StripeWebhook {
#[serde(rename = "type")]
pub event_type: String,
pub data: StripeEventData,
}
#[derive(Debug, Deserialize)]
pub struct StripeEventData {
pub object: serde_json::Value,
}
pub async fn webhook(
State(state): State<AppState>,
headers: HeaderMap,
body: String,
) -> Result<StatusCode, StatusCode> {
let stripe_secret = std::env::var("STRIPE_WEBHOOK_SECRET").unwrap_or_default();
// Verify webhook signature if configured
if !stripe_secret.is_empty() {
let sig = headers
.get("stripe-signature")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::BAD_REQUEST)?;
// In production, verify signature using Stripe library
// For MVP, we log and process
tracing::info!("Webhook signature: {}", sig);
}
let event: serde_json::Value = serde_json::from_str(&body).map_err(|_| StatusCode::BAD_REQUEST)?;
let event_type = event["type"].as_str().unwrap_or("");
match event_type {
"checkout.session.completed" => {
if let Some(metadata) = event["data"]["object"]["metadata"].as_object() {
if let Some(user_id_str) = metadata.get("user_id").and_then(|v| v.as_str()) {
if let Ok(user_id) = uuid::Uuid::parse_str(user_id_str) {
let customer_id = event["data"]["object"]["customer"].as_str().unwrap_or("");
let subscription_id = event["data"]["object"]["subscription"].as_str().unwrap_or("");
let _ = subscriptions::create_or_update(
&state.db,
user_id,
Some(customer_id),
Some(subscription_id),
None,
"active",
"paid",
).await;
tracing::info!("Subscription activated for user {}", user_id);
}
}
}
}
"invoice.payment_succeeded" => {
tracing::info!("Invoice payment succeeded");
}
"customer.subscription.deleted" => {
if let Some(sub_id) = event["data"]["object"]["id"].as_str() {
let _ = subscriptions::update_status(&state.db, sub_id, "canceled").await;
tracing::info!("Subscription {} canceled", sub_id);
}
}
_ => {}
}
Ok(StatusCode::OK)
}