add: better stripe endpoint

This commit is contained in:
trisua 2025-07-12 16:30:57 -04:00
parent 227cd3d2ac
commit fdaa81422a
15 changed files with 118 additions and 778 deletions

View file

@ -2362,7 +2362,7 @@
(sup (a ("href" "#footnote-1") (text "1"))))
(text "{%- endif %}"))
(a
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("href" "{{ config.stripe.payment_links.supporter }}?client_reference_id={{ user.id }}")
("class" "button")
("target" "_blank")
(text "Become a supporter ({{ config.stripe.supporter_price_text }})"))

View file

@ -20,7 +20,11 @@
(div
("class" "card flex flex-col gap-2")
(span
("class" "fade")
(text "{{ text \"auth:label.private_profile_message\" }}"))
(span
("class" "no_p_margin")
(text "{{ profile.settings.private_biography|markdown|safe }}"))
(div
("class" "card w-full secondary flex gap-2")
(text "{% if user -%} {% if not is_following -%}")

View file

@ -1433,6 +1433,15 @@
settings.biography,
\"textarea\",
],
[
[\"private_biography\", \"Private biography\"],
settings.private_biography,
\"textarea\",
{
embed_html:
'<span class=\"fade\">This biography is only shown to users you are not following while your account is private.</span>',
},
],
[[\"status\", \"Status\"], settings.status, \"textarea\"],
[
[\"warning\", \"Profile warning\"],

View file

@ -17,9 +17,10 @@ pub async fn stripe_webhook(
) -> impl IntoResponse {
let data = &(data.read().await).0;
if data.0.0.stripe.is_none() {
return Json(Error::MiscError("Disabled".to_string()).into());
}
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,
@ -56,7 +57,7 @@ pub async fn stripe_webhook(
Err(e) => return Json(e.into()),
};
tracing::info!("subscribe {} (stripe: {})", user.id, customer_id);
tracing::info!("payment {} (stripe: {})", user.id, customer_id);
if let Err(e) = data
.update_user_stripe_id(user.id, customer_id.as_str())
.await
@ -74,6 +75,48 @@ pub async fn stripe_webhook(
};
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;
@ -118,45 +161,54 @@ pub async fn stripe_webhook(
}
let user = user.unwrap();
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: (),
});
}
if product_id == stripe_cnf.product_ids.supporter {
// supporter
tracing::info!("found subscription user in {retries} tries");
tracing::info!("invoice {} (stripe: {})", user.id, customer_id);
let new_user_permissions = user.permissions | FinePermission::SUPPORTER;
if user.permissions.check(FinePermission::SUPPORTER) {
return Json(ApiReturn {
ok: true,
message: "Already applied".to_string(),
payload: (),
});
}
if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.await
{
return Json(e.into());
}
tracing::info!("invoice {} (stripe: {})", user.id, customer_id);
let new_user_permissions = user.permissions | FinePermission::SUPPORTER;
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)
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.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());
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 => {

View file

@ -1,175 +0,0 @@
use crate::{
get_user_from_token,
routes::{
api::v1::{CreateLayout, UpdateLayoutName, UpdateLayoutPages, UpdateLayoutPrivacy},
},
State,
};
use axum::{extract::Path, response::IntoResponse, Extension, Json};
use axum_extra::extract::CookieJar;
use tetratto_core::{
model::{
layouts::{Layout, LayoutPrivacy},
oauth,
permissions::FinePermission,
ApiReturn, Error,
},
};
pub async fn get_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadStacks) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let layout = match data.get_layout_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if layout.privacy == LayoutPrivacy::Public
&& user.id != layout.owner
&& !user.permissions.check(FinePermission::MANAGE_USERS)
{
return Json(Error::NotAllowed.into());
}
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(layout),
})
}
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadLayouts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.get_layouts_by_user(user.id).await {
Ok(x) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}
pub async fn create_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(req): Json<CreateLayout>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateLayouts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data
.create_layout(Layout::new(req.name, user.id, req.replaces))
.await
{
Ok(s) => Json(ApiReturn {
ok: true,
message: "Layout created".to_string(),
payload: s.id.to_string(),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_name_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateLayoutName>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageLayouts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_layout_title(id, &user, &req.name).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Layout updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_privacy_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateLayoutPrivacy>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageLayouts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_layout_privacy(id, &user, req.privacy).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Layout updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_pages_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateLayoutPages>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageLayouts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_layout_pages(id, &user, req.pages).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Layout updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn delete_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageLayouts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.delete_layout(id, &user).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Layout deleted".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -4,7 +4,6 @@ pub mod channels;
pub mod communities;
pub mod domains;
pub mod journals;
pub mod layouts;
pub mod notes;
pub mod notifications;
pub mod reactions;
@ -29,7 +28,6 @@ use tetratto_core::model::{
},
communities_permissions::CommunityPermission,
journals::JournalPrivacyPermission,
layouts::{CustomizablePage, LayoutPage, LayoutPrivacy},
littleweb::{DomainData, DomainTld, ServiceFsEntry},
oauth::AppScope,
permissions::{FinePermission, SecondaryPermission},
@ -625,17 +623,6 @@ pub fn routes() -> Router {
// uploads
.route("/uploads/{id}", get(uploads::get_request))
.route("/uploads/{id}", delete(uploads::delete_request))
// layouts
.route("/layouts", get(layouts::list_request))
.route("/layouts", post(layouts::create_request))
.route("/layouts/{id}", get(layouts::get_request))
.route("/layouts/{id}", delete(layouts::delete_request))
.route("/layouts/{id}/title", post(layouts::update_name_request))
.route(
"/layouts/{id}/privacy",
post(layouts::update_privacy_request),
)
.route("/layouts/{id}/pages", post(layouts::update_pages_request))
// services
.route("/services", get(services::list_request))
.route("/services", post(services::create_request))
@ -1055,27 +1042,6 @@ pub struct AwardAchievement {
pub name: AchievementName,
}
#[derive(Deserialize)]
pub struct CreateLayout {
pub name: String,
pub replaces: CustomizablePage,
}
#[derive(Deserialize)]
pub struct UpdateLayoutName {
pub name: String,
}
#[derive(Deserialize)]
pub struct UpdateLayoutPrivacy {
pub privacy: LayoutPrivacy,
}
#[derive(Deserialize)]
pub struct UpdateLayoutPages {
pub pages: Vec<LayoutPage>,
}
#[derive(Deserialize)]
pub struct CreateService {
pub name: String,