mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
compiles with endpoint logic
This commit is contained in:
1 parent
fe46c72d06
commit
35554c6dde
14 files changed
+615
-20
No files matched your search
Generated
+21
@@ -1028,6 +1028,23 @@ dependencies = [
|
||||
"smol_str 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stripe-webhook"
|
||||
version = "1.0.0-alpha.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75f8382af92463fa8e0b86e825d7d08eecbcd8715c197f1a66173fcc18e35774"
|
||||
dependencies = [
|
||||
"async-stripe-shared",
|
||||
"async-stripe-types",
|
||||
"chrono",
|
||||
"hex",
|
||||
"hmac 0.12.1",
|
||||
"miniserde",
|
||||
"serde",
|
||||
"sha2 0.10.8",
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
@@ -1307,14 +1324,18 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-stripe 1.0.0-alpha.2",
|
||||
"async-stripe-core",
|
||||
"async-stripe-webhook",
|
||||
"chrono",
|
||||
"errors",
|
||||
"http_server_common",
|
||||
"log",
|
||||
"mockall",
|
||||
"mysql_queries",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
publish = false
|
||||
name = "billing_artcraft_component"
|
||||
edition = "2018"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
@@ -22,9 +22,8 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
#mysql_queries.workspace = true
|
||||
#http_server_common = { path = "../../../lib/deprecated/http_server_common" }
|
||||
#mysql_queries = { path = "../../../schema/database/mysql_queries" }
|
||||
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
|
||||
mysql_queries = { path = "../../../schema/database/mysql_queries" }
|
||||
#reusable_types = { path = "../../../schema/public/reusable_types" }
|
||||
#url_config = { path = "../../../lib/url_config" }
|
||||
#user_traits_component = { path = "../user_traits_component" }
|
||||
@@ -39,11 +38,13 @@ once_cell.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
|
||||
|
||||
# Money
|
||||
#async-stripe = { version = "1.0.0-alpha.2", features = ["runtime-tokio-hyper"] }
|
||||
async-stripe = { version = "1.0.0-alpha.2", features = [] }
|
||||
async-stripe-core = { version = "1.0.0-alpha.2", features = ["customer"] }
|
||||
async-stripe-webhook = { version = "1.0.0-alpha.2", features = [] }
|
||||
|
||||
#sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] }
|
||||
#utoipa = { version = "4", features = ["chrono","actix_extras"] }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod webhook;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod stripe_artcraft_webhook_handler;
|
||||
pub mod stripe_artcraft_webhook_summary;
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
use actix_web::web::{Bytes, Data, Json};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
use crate::endpoints::webhook::stripe_artcraft_webhook_summary::StripeArtcraftWebhookSummary;
|
||||
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
use crate::utils::common_web_error::CommonWebError;
|
||||
use crate::utils::verify_stripe_webhook_ip_address::verify_stripe_webhook_ip_address;
|
||||
use chrono::NaiveDateTime;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::queries::billing::stripe::get_stripe_webhook_event_log_by_id::{get_stripe_webhook_event_log_by_id, get_stripe_webhook_event_log_by_id_with_connection};
|
||||
use mysql_queries::queries::billing::stripe::insert_stripe_webhook_event_log::InsertStripeWebhookEventLog;
|
||||
use serde_derive::Serialize;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::{MySql, MySqlConnection, MySqlPool};
|
||||
use std::sync::Arc;
|
||||
use stripe_webhook::{Event, Webhook};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StripeArtcraftWebhookSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// /// Stripe (Artcraft) Webhook Handler
|
||||
// #[utoipa::path(
|
||||
// post,
|
||||
// tag = "Stripe (Artcraft)",
|
||||
// path = "/v1/stripe_artcraft/webhook",
|
||||
// responses(
|
||||
// (status = 200, description = "Success", body = StripeArtcraftWebhookSuccessResponse),
|
||||
// ),
|
||||
// params()
|
||||
// )]
|
||||
pub async fn stripe_artcraft_webhook_handler(
|
||||
http_request: HttpRequest,
|
||||
//server_state: Data<Arc<ServerState>>,
|
||||
stripe_config: Data<ArtcraftStripeConfigWithClient>,
|
||||
request_body_bytes: Bytes,
|
||||
mysql_pool: Data<MySqlPool>,
|
||||
) -> Result<Json<StripeArtcraftWebhookSuccessResponse>, CommonWebError>
|
||||
{
|
||||
verify_stripe_webhook_ip_address(&http_request)
|
||||
.map_err(|e| {
|
||||
let reason = format!("Improper client IP address. Error: {:?}", e);
|
||||
error!("{}", &reason);
|
||||
CommonWebError::BadInputWithSimpleMessage("bad request".to_string())
|
||||
})?;
|
||||
|
||||
let secret_signing_key = &stripe_config.secret_webhook_signing_key;
|
||||
|
||||
let stripe_signature = get_request_header_optional(&http_request, "Stripe-Signature")
|
||||
.unwrap_or_default();
|
||||
|
||||
// NB: Treat the request payload as unstructured and defer to Stripe libraries.
|
||||
let webhook_payload = String::from_utf8(request_body_bytes.to_vec())
|
||||
.map_err(|err| {
|
||||
let reason = format!("Could not decode request body to UTF-8: {:?}", err);
|
||||
error!("{}", &reason);
|
||||
CommonWebError::BadInputWithSimpleMessage(reason)
|
||||
})?;
|
||||
|
||||
let webhook_payload = Webhook::construct_event(&webhook_payload, &stripe_signature, secret_signing_key)
|
||||
.map_err(|e| {
|
||||
let reason = format!("Could not construct Stripe webhook event: {:?}", e);
|
||||
error!("{}", &reason);
|
||||
println!("{:?}", webhook_payload);
|
||||
CommonWebError::BadInputWithSimpleMessage(reason)
|
||||
})?;
|
||||
|
||||
// Events can be re-sent, so we need to make handling them idempotent.
|
||||
let stripe_event_id = webhook_payload.id.to_string();
|
||||
|
||||
let stripe_event_created_at = NaiveDateTime::from_timestamp(webhook_payload.created, 0);
|
||||
|
||||
let stripe_event_type = webhook_payload.type_.as_str().to_string();
|
||||
|
||||
//let stripe_event_type = serde_json::to_string(&webhook_payload.type_)
|
||||
// .map(|s| s.replace("\"", ""))
|
||||
// .map_err(|err| {
|
||||
// let reason = format!("Could not serialize webhook type: {:?}", err);
|
||||
// error!("{}", &reason);
|
||||
// CommonWebError::BadInputWithSimpleMessage(reason)
|
||||
// })?;
|
||||
|
||||
// NB: Whether this was test data or live data
|
||||
let stripe_is_production = webhook_payload.livemode;
|
||||
|
||||
info!("Stripe webhook event type: {} ({:?}); is production: {}, created at: {}, pending events to be handled: {}",
|
||||
&stripe_event_type,
|
||||
&webhook_payload.type_,
|
||||
stripe_is_production,
|
||||
&stripe_event_created_at,
|
||||
webhook_payload.pending_webhooks);
|
||||
|
||||
let mut mysql_connection = mysql_pool
|
||||
.acquire()
|
||||
.await?;
|
||||
|
||||
let maybe_previously_played_event = get_stripe_webhook_event_log_by_id_with_connection(&stripe_event_id, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let reason = format!("Could not query previous event by ID ({}): {:?}", &stripe_event_id, err);
|
||||
error!("{}", &reason);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
if let Some(event) = maybe_previously_played_event {
|
||||
// The event is being replayed by Stripe, and we've already handled it.
|
||||
// We'll ignore it so that we remain idempotent.
|
||||
if event.should_ignore_retry {
|
||||
warn!("Stripe is replying event with ID={}. Ignoring it since we have previously handled it.",
|
||||
&stripe_event_id);
|
||||
|
||||
return Ok(Json(StripeArtcraftWebhookSuccessResponse {
|
||||
success: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let webhook_summary = handle_webhook_payload(&mut mysql_connection, webhook_payload, &stripe_event_type).await?;
|
||||
|
||||
let query = InsertStripeWebhookEventLog {
|
||||
stripe_event_id,
|
||||
stripe_event_type,
|
||||
maybe_stripe_event_entity_id: webhook_summary.maybe_event_entity_id,
|
||||
maybe_stripe_customer_id: webhook_summary.maybe_stripe_customer_id,
|
||||
stripe_event_created_at,
|
||||
stripe_is_production,
|
||||
maybe_user_token: webhook_summary.maybe_user_token,
|
||||
action_was_taken: webhook_summary.action_was_taken,
|
||||
should_ignore_retry: webhook_summary.should_ignore_retry,
|
||||
};
|
||||
|
||||
query.insert(&mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let reason = format!("Could not insert Stripe webhook event log: {:?}", err);
|
||||
error!("{}", &reason);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
Ok(Json(StripeArtcraftWebhookSuccessResponse {
|
||||
success: true,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn handle_webhook_payload(
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
webhook_payload: Event,
|
||||
stripe_event_type: &String
|
||||
) -> Result<StripeArtcraftWebhookSummary, CommonWebError> {
|
||||
let mut unhandled_event_type = false;
|
||||
|
||||
let mut webhook_summary = StripeArtcraftWebhookSummary {
|
||||
maybe_user_token: None,
|
||||
maybe_event_entity_id: None,
|
||||
maybe_stripe_customer_id: None,
|
||||
action_was_taken: false,
|
||||
should_ignore_retry: false,
|
||||
};
|
||||
|
||||
// NB:
|
||||
// - "Webhook endpoints might occasionally receive the same event more than once."
|
||||
// - "Stripe does not guarantee delivery of events in the order in which they are generated."
|
||||
/*
|
||||
match webhook_payload.type_ {
|
||||
|
||||
// =============== CHECKOUT SESSIONS ===============
|
||||
|
||||
EventType::CheckoutSessionCompleted => {
|
||||
if let EventObject::CheckoutSession(checkout_session) = webhook_payload.data.object {
|
||||
webhook_summary = checkout_session_completed_handler(checkout_session)?;
|
||||
}
|
||||
}
|
||||
|
||||
// EventType::CheckoutSessionExpired => {}
|
||||
// EventType::CheckoutSessionAsyncPaymentFailed => {}
|
||||
// EventType::CheckoutSessionAsyncPaymentSucceeded => {}
|
||||
|
||||
// =============== CUSTOMERS ===============
|
||||
|
||||
EventType::CustomerCreated => {
|
||||
if let EventObject::Customer(customer) = webhook_payload.data.object {
|
||||
webhook_summary = customer_created_handler(&customer)?;
|
||||
}
|
||||
}
|
||||
EventType::CustomerUpdated => {
|
||||
if let EventObject::Customer(customer) = webhook_payload.data.object {
|
||||
webhook_summary = customer_updated_handler(&customer)?;
|
||||
}
|
||||
}
|
||||
EventType::CustomerDeleted => {
|
||||
if let EventObject::Customer(customer) = webhook_payload.data.object {
|
||||
webhook_summary = customer_deleted_handler(&customer)?;
|
||||
}
|
||||
}
|
||||
|
||||
// =============== CUSTOMER SUBSCRIPTIONS ===============
|
||||
|
||||
EventType::CustomerSubscriptionCreated => {
|
||||
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
|
||||
webhook_summary = customer_subscription_created_handler(
|
||||
&subscription,
|
||||
internal_subscription_product_lookup.get_ref(),
|
||||
&mysql_pool).await?;
|
||||
}
|
||||
}
|
||||
EventType::CustomerSubscriptionUpdated => {
|
||||
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
|
||||
webhook_summary = customer_subscription_updated_handler(
|
||||
&subscription,
|
||||
internal_subscription_product_lookup.get_ref(),
|
||||
&mysql_pool).await?;
|
||||
}
|
||||
}
|
||||
EventType::CustomerSubscriptionDeleted => {
|
||||
if let EventObject::Subscription(subscription) = webhook_payload.data.object {
|
||||
webhook_summary = customer_subscription_deleted_handler(
|
||||
&subscription,
|
||||
internal_subscription_product_lookup.get_ref(),
|
||||
&mysql_pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// EventType::CustomerSubscriptionPendingUpdateApplied => {}
|
||||
// EventType::CustomerSubscriptionPendingUpdateExpired => {}
|
||||
// EventType::CustomerSubscriptionTrialWillEnd => {}
|
||||
|
||||
// =============== INVOICES ===============
|
||||
|
||||
EventType::InvoiceCreated => {
|
||||
// TODO: We need to respond to this so we don't hold payments up by 72 hours!
|
||||
// See: https://stripe.com/docs/billing/subscriptions/webhooks
|
||||
}
|
||||
EventType::InvoiceUpdated => {
|
||||
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
|
||||
webhook_summary = invoice_updated_handler(&invoice)?;
|
||||
}
|
||||
}
|
||||
EventType::InvoicePaid => {
|
||||
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
|
||||
webhook_summary = invoice_paid_handler(&invoice)?;
|
||||
}
|
||||
}
|
||||
EventType::InvoicePaymentSucceeded => {
|
||||
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
|
||||
webhook_summary = invoice_payment_succeeded_handler(&invoice)?;
|
||||
}
|
||||
}
|
||||
EventType::InvoicePaymentFailed => {
|
||||
if let EventObject::Invoice(invoice) = webhook_payload.data.object {
|
||||
webhook_summary = invoice_payment_failed_handler(&invoice)?;
|
||||
}
|
||||
}
|
||||
|
||||
// EventType::InvoiceDeleted => {}
|
||||
// EventType::InvoiceFinalizationFailed => {}
|
||||
// EventType::InvoiceFinalized => {}
|
||||
// EventType::InvoiceItemCreated => {}
|
||||
// EventType::InvoiceItemDeleted => {}
|
||||
// EventType::InvoiceItemUpdated => {}
|
||||
// EventType::InvoiceMarkedUncollectible => {}
|
||||
// EventType::InvoicePaymentActionRequired => {}
|
||||
// EventType::InvoiceSent => {}
|
||||
// EventType::InvoiceUpcoming => {}
|
||||
// EventType::InvoiceUpdated => {}
|
||||
// EventType::InvoiceVoided => {}
|
||||
|
||||
// =============== PAYMENT INTENTS ===============
|
||||
|
||||
EventType::PaymentIntentSucceeded => {
|
||||
if let EventObject::PaymentIntent(payment_intent) = webhook_payload.data.object {
|
||||
webhook_summary = payment_intent_succeeded_handler(&payment_intent)?;
|
||||
}
|
||||
}
|
||||
|
||||
// EventType::PaymentIntentAmountCapturableUpdated => {}
|
||||
// EventType::PaymentIntentCanceled => {}
|
||||
// EventType::PaymentIntentCreated => {}
|
||||
// EventType::PaymentIntentPartiallyFunded => {}
|
||||
// EventType::PaymentIntentPaymentFailed => {}
|
||||
// EventType::PaymentIntentProcessing => {}
|
||||
// EventType::PaymentIntentRequiresAction => {}
|
||||
// EventType::PaymentIntentRequiresCapture => {}
|
||||
|
||||
// =============== CHARGES ===============
|
||||
|
||||
EventType::ChargeSucceeded => {
|
||||
if let EventObject::Charge(charge) = webhook_payload.data.object {
|
||||
webhook_summary = charge_succeeded_handler(&charge)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============== Ignored ===============
|
||||
|
||||
// Ignoring these types:
|
||||
// Account* (6),
|
||||
// ApplicationFee* (2),
|
||||
// BalanceAvailable (1),
|
||||
// BillingPortal* (2),
|
||||
// CapabilityCreated (1),
|
||||
// Charge* (13),
|
||||
// [CheckoutSession* handled above],
|
||||
// Coupon* (3),
|
||||
// CreditNote* (3),
|
||||
// CustomerDiscount* (3),
|
||||
// CustomerSource* (4),
|
||||
// [CustomerSubscription* handled above],
|
||||
// CreditTax* (3),
|
||||
// FileCreated (1),
|
||||
// IdentityVerification* (6),
|
||||
// [Invoice* handled above],
|
||||
// IssuingAuthorization* (3),
|
||||
// IssuingCard* (2),
|
||||
// IssuingCardholder* (2),
|
||||
// IssuingDispute* (2),
|
||||
// IssuingTransaction* (2),
|
||||
// MandateUpdated (1),
|
||||
// Order* (6),
|
||||
// [PaymentIntent* handled above],
|
||||
// PaymentLink* (2),
|
||||
// PaymentMethod* (4),
|
||||
// Payout* (5),
|
||||
// Person* (3),
|
||||
// Plan* (3),
|
||||
// Price* (3),
|
||||
// Product* (3),
|
||||
// PromotionCode* (2),
|
||||
// Quote* (4),
|
||||
// RadarEarlyFraudWarning* (2),
|
||||
// Recipient* (3),
|
||||
// ReportingReport* (3),
|
||||
// Review* (2),
|
||||
// SetupIntent* (5), -- N/A; collecting payments in the future we don't yet have information for (eg. crowdfunding, rental car, utility bill)
|
||||
// SubscriptionSchedule* (7), -- N/A; backdate subscriptions or schedule one to start later (eg. for physical goods, like a newspaper)
|
||||
// SigmaScheduledQueryRunCreated (1),
|
||||
// Sku (3),
|
||||
// Source* (7),
|
||||
// TaxRate* (2),
|
||||
// TerminalReader* (2),
|
||||
// TestHelpersTestClock* (5),
|
||||
// Topup* (5),
|
||||
// Transfer* (5),
|
||||
|
||||
_ => {
|
||||
unhandled_event_type = true;
|
||||
},
|
||||
}
|
||||
*/
|
||||
|
||||
// To play with the payload contents as JSON:
|
||||
// let json = serde_json::ser::to_string(&event_payload).unwrap();
|
||||
// info!("event payload as json: {}", json);
|
||||
|
||||
if unhandled_event_type {
|
||||
warn!("Unhandled Stripe webhook event type: {} ({:?})",
|
||||
&stripe_event_type,
|
||||
&webhook_payload.type_);
|
||||
}
|
||||
|
||||
Ok(webhook_summary)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
/// Reports common information from the webhook handlers
|
||||
pub struct StripeArtcraftWebhookSummary {
|
||||
/// If we recorded our own internal user ID as Stripe metadata, this passes it upstream.
|
||||
pub maybe_user_token: Option<String>,
|
||||
|
||||
/// This is the core entity type associated with the webhook event.
|
||||
/// We pass this upstream so we can record it and look it up later for debugging.
|
||||
pub maybe_event_entity_id: Option<String>,
|
||||
|
||||
/// This is the stripe customer ID, if it was associated with the event.
|
||||
pub maybe_stripe_customer_id: Option<String>,
|
||||
|
||||
/// Whether we took any sort of action in response to the webhook.
|
||||
/// Not all event types have handlers yet, and even so, sometimes we may choose to do nothing.
|
||||
pub action_was_taken: bool,
|
||||
|
||||
/// Whether any retries should be dropped.
|
||||
/// This helps us handle event with idempotency in the event Stripe replays them (and it can!)
|
||||
pub should_ignore_retry: bool,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod endpoints;
|
||||
pub mod utils;
|
||||
@@ -0,0 +1,30 @@
|
||||
use stripe::Client;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ArtcraftStripeConfig {
|
||||
pub secret_key: String,
|
||||
pub secret_webhook_signing_key: String,
|
||||
pub checkout_success_url: String,
|
||||
pub checkout_cancel_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ArtcraftStripeConfigWithClient {
|
||||
pub secret_key: String,
|
||||
pub secret_webhook_signing_key: String,
|
||||
pub checkout_success_url: String,
|
||||
pub checkout_cancel_url: String,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl ArtcraftStripeConfig {
|
||||
pub fn to_config_with_client(&self) -> ArtcraftStripeConfigWithClient {
|
||||
ArtcraftStripeConfigWithClient {
|
||||
secret_key: self.secret_key.clone(),
|
||||
secret_webhook_signing_key: self.secret_webhook_signing_key.clone(),
|
||||
checkout_success_url: self.checkout_success_url.clone(),
|
||||
checkout_cancel_url: self.checkout_cancel_url.clone(),
|
||||
client: Client::new(self.secret_key.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use log::error;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommonWebError {
|
||||
// TODO: Bad input with error fields vector.
|
||||
/// Bad input with a user-facing error message.
|
||||
BadInputWithSimpleMessage(String),
|
||||
/// Entity not found.
|
||||
NotFound,
|
||||
/// Not authorized to perform the action.
|
||||
NotAuthorized,
|
||||
/// Server error with no reason.
|
||||
ServerError,
|
||||
/// Payment required with no extra info
|
||||
PaymentRequired,
|
||||
}
|
||||
|
||||
impl Error for CommonWebError {}
|
||||
|
||||
impl Display for CommonWebError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializeSimpleErrorWithoutMessage<'a> {
|
||||
success: bool,
|
||||
error_code: u16,
|
||||
error_code_str: Option<&'a str>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializeSimpleErrorWithMessage<'a> {
|
||||
success: bool,
|
||||
error_code: u16,
|
||||
error_code_str: Option<&'a str>,
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
impl ResponseError for CommonWebError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
CommonWebError::BadInputWithSimpleMessage(_) => StatusCode::BAD_REQUEST,
|
||||
CommonWebError::NotFound => StatusCode::NOT_FOUND,
|
||||
CommonWebError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
CommonWebError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
CommonWebError::PaymentRequired => StatusCode::PAYMENT_REQUIRED,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
CommonWebError::BadInputWithSimpleMessage(msg) => {
|
||||
HttpResponse::BadRequest()
|
||||
.json(SerializeSimpleErrorWithMessage {
|
||||
success: false,
|
||||
error_code: self.status_code().as_u16(),
|
||||
error_code_str: self.status_code().canonical_reason(),
|
||||
message: msg,
|
||||
})
|
||||
},
|
||||
_ => {
|
||||
HttpResponseBuilder::new(self.status_code())
|
||||
.json(SerializeSimpleErrorWithoutMessage {
|
||||
success: false,
|
||||
error_code: self.status_code().as_u16(),
|
||||
error_code_str: self.status_code().canonical_reason(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for CommonWebError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
error!("SQLx error: {:?}", value);
|
||||
CommonWebError::ServerError
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use actix_http::body::MessageBody;
|
||||
|
||||
#[test]
|
||||
fn test_bad_input_json() {
|
||||
let error = CommonWebError::BadInputWithSimpleMessage("foo bar baz".to_string());
|
||||
let response = error.error_response();
|
||||
let bytes = response.into_body().try_into_bytes().unwrap();
|
||||
let body = String::from_utf8(bytes.to_vec()).unwrap();
|
||||
assert_eq!(body, "{\"success\":false,\"error_code\":400,\"error_code_str\":\"Bad Request\",\"message\":\"foo bar baz\"}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod artcraft_stripe_config;
|
||||
pub mod common_web_error;
|
||||
pub mod verify_stripe_webhook_ip_address;
|
||||
@@ -0,0 +1,48 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use actix_web::HttpRequest;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
|
||||
/// List of IP addresses that send webhook requests
|
||||
/// From: https://stripe.com/docs/ips
|
||||
static STRIPE_WEBHOOK_IP_ADDRESSES : Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
let mut ip_addresses = HashSet::new();
|
||||
|
||||
ip_addresses.extend([
|
||||
"3.18.12.63",
|
||||
"3.130.192.231",
|
||||
"13.235.14.237",
|
||||
"13.235.122.149",
|
||||
"18.211.135.69",
|
||||
"35.154.171.200",
|
||||
"52.15.183.38",
|
||||
"54.88.130.119",
|
||||
"54.88.130.237",
|
||||
"54.187.174.169",
|
||||
"54.187.205.235",
|
||||
"54.187.216.72",
|
||||
].map(|s| s.to_string()));
|
||||
|
||||
ip_addresses
|
||||
});
|
||||
|
||||
/// Verify that the request comes from a Stripe webhook client IP
|
||||
/// Recommendation from: https://stripe.com/docs/webhooks/best-practices
|
||||
pub fn verify_stripe_webhook_ip_address(http_request: &HttpRequest) -> AnyhowResult<()> {
|
||||
let ip_address = get_request_ip(http_request);
|
||||
|
||||
if STRIPE_WEBHOOK_IP_ADDRESSES.contains(&ip_address) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// TODO: Only allow this in development
|
||||
if ip_address == "127.0.0.1" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(anyhow!("Not a valid Stripe webhook IP address: {:?}", &ip_address))
|
||||
}
|
||||
+11
-7
@@ -180,15 +180,19 @@ pub async fn stripe_artcraft_create_checkout_session_handler(
|
||||
}
|
||||
}
|
||||
|
||||
CheckoutSession::create(&server_state.stripe_artcraft.client, params)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Stripe Error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?
|
||||
// TODO: THis is now broken
|
||||
//CheckoutSession::create(&server_state.stripe_artcraft.client, params)
|
||||
// .await
|
||||
// .map_err(|e| {
|
||||
// error!("Stripe Error: {:?}", e);
|
||||
// CommonWebError::ServerError
|
||||
// })?
|
||||
()
|
||||
};
|
||||
|
||||
let url = checkout_session.url.ok_or(CommonWebError::ServerError)?;
|
||||
//let url = checkout_session.url.ok_or(CommonWebError::ServerError)?;
|
||||
|
||||
let url = "todo".to_string();
|
||||
|
||||
// Best effort to delete Redis session cache
|
||||
internal_session_cache_purge.best_effort_purge_session_cache(&http_request);
|
||||
|
||||
@@ -45,6 +45,7 @@ use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoint_filte
|
||||
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::disabled_endpoints::DisabledEndpoints;
|
||||
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::exact_match_disabled_endpoints::ExactMatchDisabledEndpoints;
|
||||
use actix_helpers::middleware::disabled_endpoint_filter::disabled_endpoints::prefix_disabled_endpoints::PrefixDisabledEndpoints;
|
||||
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfig;
|
||||
use billing_component::stripe::stripe_config::{FullUrlOrPath, StripeCheckoutConfigs, StripeConfig, StripeCustomerPortalConfigs, StripeSecrets};
|
||||
use billing_component::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup;
|
||||
use billing_component::stripe::traits::internal_subscription_product_lookup::InternalSubscriptionProductLookup;
|
||||
@@ -413,17 +414,12 @@ async fn main() -> AnyhowResult<()> {
|
||||
client: stripe_client,
|
||||
},
|
||||
stripe_artcraft: {
|
||||
let secret_key = easyenv::get_env_string_required("STRIPE_ARTCRAFT_SECRET_KEY")?;
|
||||
StripeArtcraftSettings {
|
||||
secret_key: secret_key.clone(),
|
||||
ArtcraftStripeConfig {
|
||||
secret_key: easyenv::get_env_string_required("STRIPE_ARTCRAFT_SECRET_KEY")?,
|
||||
secret_webhook_signing_key: easyenv::get_env_string_required("STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY")?,
|
||||
checkout_success_url: easyenv::get_env_string_required("STRIPE_ARTCRAFT_CHECKOUT_SUCCESS_URL")?,
|
||||
checkout_cancel_url: easyenv::get_env_string_required("STRIPE_ARTCRAFT_CHECKOUT_CANCEL_URL")?,
|
||||
client: {
|
||||
let api_secret = secret_key.clone();
|
||||
async_stripe_artcraft::Client::new(api_secret)
|
||||
},
|
||||
}
|
||||
}.to_config_with_client()
|
||||
},
|
||||
hostname: server_hostname,
|
||||
startup_time,
|
||||
@@ -623,6 +619,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
|
||||
|
||||
// NB: app_data being clone()'d below should all be safe (dependencies included)
|
||||
let app = App::new()
|
||||
.app_data(web::Data::new(server_state_arc.stripe_artcraft.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.firehose_publisher.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.mysql_pool.clone()))
|
||||
.app_data(web::Data::new(server_state_arc.redis_pool.clone()))
|
||||
|
||||
@@ -5,6 +5,7 @@ use sqlx::MySqlPool;
|
||||
|
||||
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSet;
|
||||
use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList;
|
||||
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
use billing_component::stripe::stripe_config::StripeConfig;
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use email_sender::smtp_email_sender::SmtpEmailSender;
|
||||
@@ -49,7 +50,7 @@ pub struct ServerState {
|
||||
pub server_info: ServerInfo,
|
||||
|
||||
pub stripe: StripeSettings,
|
||||
pub stripe_artcraft: StripeArtcraftSettings,
|
||||
pub stripe_artcraft: ArtcraftStripeConfigWithClient,
|
||||
|
||||
pub hostname: String,
|
||||
|
||||
|
||||
Reference in new issue
Block a user