474 lines
16 KiB
Rust
474 lines
16 KiB
Rust
use std::{str::FromStr, time::Duration};
|
|
|
|
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
|
|
use axum_extra::extract::CookieJar;
|
|
use tetratto_core::model::{
|
|
auth::{User, Notification},
|
|
moderation::AuditLogEntry,
|
|
permissions::FinePermission,
|
|
ApiReturn, Error,
|
|
};
|
|
use stripe::{EventObject, EventType};
|
|
use crate::{get_user_from_token, State};
|
|
|
|
pub async fn stripe_webhook(
|
|
Extension(data): Extension<State>,
|
|
headers: HeaderMap,
|
|
body: String,
|
|
) -> impl IntoResponse {
|
|
let data = &(data.read().await).0;
|
|
|
|
let stripe_cnf = match data.0.0.stripe {
|
|
Some(ref c) => c,
|
|
None => return Json(Error::MiscError("Disabled".to_string()).into()),
|
|
};
|
|
|
|
let sig = match headers.get("Stripe-Signature") {
|
|
Some(s) => s,
|
|
None => return Json(Error::NotAllowed.into()),
|
|
};
|
|
|
|
let req = match stripe::Webhook::construct_event(
|
|
&body,
|
|
sig.to_str().unwrap(),
|
|
&data.0.0.stripe.as_ref().unwrap().webhook_signing_secret,
|
|
) {
|
|
Ok(e) => e,
|
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
|
};
|
|
|
|
match req.type_ {
|
|
EventType::CheckoutSessionCompleted => {
|
|
// checkout session ended, store user customer id (from stripe) in their user data
|
|
let session = match req.data.object {
|
|
EventObject::CheckoutSession(c) => c,
|
|
_ => unreachable!("cannot be this"),
|
|
};
|
|
|
|
let user_id = session
|
|
.client_reference_id
|
|
.unwrap()
|
|
.parse::<usize>()
|
|
.unwrap();
|
|
|
|
let customer_id = session.customer.unwrap().id();
|
|
|
|
let user = match data.get_user_by_id(user_id).await {
|
|
Ok(ua) => ua,
|
|
Err(e) => return Json(e.into()),
|
|
};
|
|
|
|
tracing::info!("payment {} (stripe: {})", user.id, customer_id);
|
|
if let Err(e) = data
|
|
.update_user_stripe_id(user.id, customer_id.as_str())
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
}
|
|
EventType::InvoicePaymentSucceeded => {
|
|
// payment finished and subscription created
|
|
// we're doing this *instead* of CustomerSubscriptionCreated because
|
|
// the invoice happens *after* the checkout session ends... which is what we need
|
|
let invoice = match req.data.object {
|
|
EventObject::Invoice(c) => c,
|
|
_ => unreachable!("cannot be this"),
|
|
};
|
|
|
|
let customer_id = invoice.customer.unwrap().id();
|
|
let lines = invoice.lines.unwrap();
|
|
|
|
if lines.total_count.unwrap() > 1 {
|
|
if let Err(e) = data
|
|
.create_audit_log_entry(AuditLogEntry::new(
|
|
0,
|
|
format!("too many invoice line items: stripe {customer_id}"),
|
|
))
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
|
|
return Json(Error::MiscError("Too many line items".to_string()).into());
|
|
}
|
|
|
|
let item = match lines.data.get(0) {
|
|
Some(i) => i,
|
|
None => {
|
|
if let Err(e) = data
|
|
.create_audit_log_entry(AuditLogEntry::new(
|
|
0,
|
|
format!("too few invoice line items: stripe {customer_id}"),
|
|
))
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
|
|
return Json(Error::MiscError("Too few line items".to_string()).into());
|
|
}
|
|
};
|
|
|
|
let product_id = item
|
|
.price
|
|
.as_ref()
|
|
.unwrap()
|
|
.product
|
|
.as_ref()
|
|
.unwrap()
|
|
.id()
|
|
.to_string();
|
|
|
|
// pull user and update role
|
|
let mut retries: usize = 0;
|
|
let mut user: Option<User> = None;
|
|
|
|
loop {
|
|
if retries >= 5 {
|
|
// we've already tried 5 times (25 seconds of waiting)... it's not
|
|
// going to happen
|
|
//
|
|
// we're going to report this error to the audit log so someone can
|
|
// check manually later
|
|
if let Err(e) = data
|
|
.create_audit_log_entry(AuditLogEntry::new(
|
|
0,
|
|
format!("invoice tier update failed: stripe {customer_id}"),
|
|
))
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
|
|
return Json(Error::GeneralNotFound("user".to_string()).into());
|
|
}
|
|
|
|
match data.get_user_by_stripe_id(customer_id.as_str()).await {
|
|
Ok(ua) => {
|
|
if !user.is_none() {
|
|
break;
|
|
}
|
|
|
|
user = Some(ua);
|
|
break;
|
|
}
|
|
Err(_) => {
|
|
tracing::info!("checkout session not stored in db yet");
|
|
retries += 1;
|
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
let user = user.unwrap();
|
|
|
|
if product_id == stripe_cnf.product_ids.supporter {
|
|
// supporter
|
|
tracing::info!("found subscription user in {retries} tries");
|
|
|
|
if user.permissions.check(FinePermission::SUPPORTER) {
|
|
return Json(ApiReturn {
|
|
ok: true,
|
|
message: "Already applied".to_string(),
|
|
payload: (),
|
|
});
|
|
}
|
|
|
|
tracing::info!("invoice {} (stripe: {})", user.id, customer_id);
|
|
let new_user_permissions = user.permissions | FinePermission::SUPPORTER;
|
|
|
|
if let Err(e) = data
|
|
.update_user_role(user.id, new_user_permissions, user.clone(), true)
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
|
|
if data.0.0.security.enable_invite_codes && user.awaiting_purchase {
|
|
if let Err(e) = data
|
|
.update_user_awaiting_purchased_status(user.id, false, user.clone(), false)
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
}
|
|
|
|
if let Err(e) = data
|
|
.create_notification(Notification::new(
|
|
"Welcome new supporter!".to_string(),
|
|
"Thank you for your support! Your account has been updated with your new role."
|
|
.to_string(),
|
|
user.id,
|
|
))
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
} else {
|
|
tracing::error!(
|
|
"received an invalid stripe product id, please check config.stripe.product_ids"
|
|
);
|
|
return Json(Error::MiscError("Unknown product ID".to_string()).into());
|
|
}
|
|
}
|
|
EventType::CustomerSubscriptionDeleted => {
|
|
// payment failed and subscription deleted
|
|
let subscription = match req.data.object {
|
|
EventObject::Subscription(c) => c,
|
|
_ => unreachable!("cannot be this"),
|
|
};
|
|
|
|
let customer_id = subscription.customer.id();
|
|
|
|
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
|
|
Ok(ua) => ua,
|
|
Err(e) => return Json(e.into()),
|
|
};
|
|
|
|
tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id);
|
|
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
|
|
|
|
if let Err(e) = data
|
|
.update_user_role(user.id, new_user_permissions, user.clone(), true)
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
|
|
if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0
|
|
{
|
|
// user doesn't come from an invite code, and is a purchased account
|
|
// this means their account must be locked if they stop paying
|
|
if let Err(e) = data
|
|
.update_user_awaiting_purchased_status(user.id, true, user.clone(), false)
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
}
|
|
|
|
if let Err(e) = data
|
|
.create_notification(Notification::new(
|
|
"Sorry to see you go... :(".to_string(),
|
|
"Thank you for your past support! Please feel free to leave us feedback on why you decided to cancel."
|
|
.to_string(),
|
|
user.id,
|
|
))
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
}
|
|
EventType::InvoicePaymentFailed => {
|
|
// payment failed
|
|
let invoice = match req.data.object {
|
|
EventObject::Invoice(i) => i,
|
|
_ => unreachable!("cannot be this"),
|
|
};
|
|
|
|
let customer_id = invoice.customer.expect("TETRATTO_STRIPE_NO_CUSTOMER").id();
|
|
|
|
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
|
|
Ok(ua) => ua,
|
|
Err(e) => return Json(e.into()),
|
|
};
|
|
|
|
if !user.permissions.check(FinePermission::SUPPORTER) {
|
|
// the user isn't currently a supporter, there's no reason to send this notification
|
|
return Json(ApiReturn {
|
|
ok: true,
|
|
message: "Acceptable".to_string(),
|
|
payload: (),
|
|
});
|
|
}
|
|
|
|
tracing::info!(
|
|
"unsubscribe (pay fail) {} (stripe: {})",
|
|
user.id,
|
|
customer_id
|
|
);
|
|
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
|
|
|
|
if let Err(e) = data
|
|
.update_user_role(user.id, new_user_permissions, user.clone(), true)
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
|
|
if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0
|
|
{
|
|
// user doesn't come from an invite code, and is a purchased account
|
|
// this means their account must be locked if they stop paying
|
|
if let Err(e) = data
|
|
.update_user_awaiting_purchased_status(user.id, true, user.clone(), false)
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
}
|
|
|
|
if let Err(e) = data
|
|
.create_notification(Notification::new(
|
|
"It seems your recent payment has failed :(".to_string(),
|
|
"No worries! Your subscription is still active and will be retried. Your supporter status will resume when you have a successful payment."
|
|
.to_string(),
|
|
user.id,
|
|
))
|
|
.await
|
|
{
|
|
return Json(e.into());
|
|
}
|
|
}
|
|
_ => return Json(Error::Unknown.into()),
|
|
}
|
|
|
|
Json(ApiReturn {
|
|
ok: true,
|
|
message: "Acceptable".to_string(),
|
|
payload: (),
|
|
})
|
|
}
|
|
|
|
pub async fn onboarding_account_link_request(
|
|
jar: CookieJar,
|
|
Extension(data): Extension<State>,
|
|
) -> impl IntoResponse {
|
|
let data = &(data.read().await);
|
|
let user = match get_user_from_token!(jar, data.0) {
|
|
Some(ua) => ua,
|
|
None => return Json(Error::NotAllowed.into()),
|
|
};
|
|
|
|
if user.seller_data.account_id.is_some() {
|
|
return Json(Error::NotAllowed.into());
|
|
}
|
|
|
|
let client = match data.3 {
|
|
Some(ref c) => c,
|
|
None => return Json(Error::Unknown.into()),
|
|
};
|
|
|
|
match stripe::AccountLink::create(
|
|
&client,
|
|
stripe::CreateAccountLink {
|
|
account: match user.seller_data.account_id {
|
|
Some(id) => stripe::AccountId::from_str(&id).unwrap(),
|
|
None => return Json(Error::NotAllowed.into()),
|
|
},
|
|
type_: stripe::AccountLinkType::AccountOnboarding,
|
|
collect: None,
|
|
expand: &[],
|
|
refresh_url: Some(&format!(
|
|
"{}/auth/connections_link/seller/refresh",
|
|
data.0.0.0.host
|
|
)),
|
|
return_url: Some(&format!(
|
|
"{}/auth/connections_link/seller/return",
|
|
data.0.0.0.host
|
|
)),
|
|
collection_options: None,
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
Ok(x) => Json(ApiReturn {
|
|
ok: true,
|
|
message: "Acceptable".to_string(),
|
|
payload: Some(x.url),
|
|
}),
|
|
Err(e) => Json(Error::MiscError(e.to_string()).into()),
|
|
}
|
|
}
|
|
|
|
pub async fn create_seller_account_request(
|
|
jar: CookieJar,
|
|
Extension(data): Extension<State>,
|
|
) -> impl IntoResponse {
|
|
let data = &(data.read().await);
|
|
let mut user = match get_user_from_token!(jar, data.0) {
|
|
Some(ua) => ua,
|
|
None => return Json(Error::NotAllowed.into()),
|
|
};
|
|
|
|
if user.seller_data.account_id.is_some() {
|
|
return Json(Error::NotAllowed.into());
|
|
}
|
|
|
|
let client = match data.3 {
|
|
Some(ref c) => c,
|
|
None => return Json(Error::Unknown.into()),
|
|
};
|
|
|
|
let account = match stripe::Account::create(
|
|
&client,
|
|
stripe::CreateAccount {
|
|
type_: Some(stripe::AccountType::Express),
|
|
capabilities: Some(stripe::CreateAccountCapabilities {
|
|
card_payments: Some(stripe::CreateAccountCapabilitiesCardPayments {
|
|
requested: Some(true),
|
|
}),
|
|
transfers: Some(stripe::CreateAccountCapabilitiesTransfers {
|
|
requested: Some(true),
|
|
}),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
Ok(a) => a,
|
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
|
};
|
|
|
|
user.seller_data.account_id = Some(account.id.to_string());
|
|
match data
|
|
.0
|
|
.update_user_seller_data(user.id, user.seller_data)
|
|
.await
|
|
{
|
|
Ok(_) => Json(ApiReturn {
|
|
ok: true,
|
|
message: "Acceptable".to_string(),
|
|
payload: (),
|
|
}),
|
|
Err(e) => return Json(e.into()),
|
|
}
|
|
}
|
|
|
|
pub async fn login_link_request(
|
|
jar: CookieJar,
|
|
Extension(data): Extension<State>,
|
|
) -> impl IntoResponse {
|
|
let data = &(data.read().await);
|
|
let user = match get_user_from_token!(jar, data.0) {
|
|
Some(ua) => ua,
|
|
None => return Json(Error::NotAllowed.into()),
|
|
};
|
|
|
|
if user.seller_data.account_id.is_none() | !user.seller_data.completed_onboarding {
|
|
return Json(Error::NotAllowed.into());
|
|
}
|
|
|
|
let client = match data.3 {
|
|
Some(ref c) => c,
|
|
None => return Json(Error::Unknown.into()),
|
|
};
|
|
|
|
match stripe::LoginLink::create(
|
|
&client,
|
|
&stripe::AccountId::from_str(&user.seller_data.account_id.unwrap()).unwrap(),
|
|
&data.0.0.0.host,
|
|
)
|
|
.await
|
|
{
|
|
Ok(x) => Json(ApiReturn {
|
|
ok: true,
|
|
message: "Acceptable".to_string(),
|
|
payload: Some(x.url),
|
|
}),
|
|
Err(e) => Json(Error::MiscError(e.to_string()).into()),
|
|
}
|
|
}
|