tetratto/crates/app/src/routes/api/v1/auth/connections/stripe.rs

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()),
}
}