add: developer pass
This commit is contained in:
parent
636ecce9f4
commit
02f3d08926
14 changed files with 355 additions and 101 deletions
|
@ -15,7 +15,7 @@ use tetratto_core::{
|
|||
config::Config,
|
||||
model::{
|
||||
auth::{DefaultTimelineChoice, User},
|
||||
permissions::FinePermission,
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
},
|
||||
};
|
||||
use tetratto_l10n::LangFile;
|
||||
|
@ -516,6 +516,11 @@ pub(crate) async fn initial_context(
|
|||
"is_supporter",
|
||||
&ua.permissions.check(FinePermission::SUPPORTER),
|
||||
);
|
||||
ctx.insert(
|
||||
"has_developer_pass",
|
||||
&ua.secondary_permissions
|
||||
.check(SecondaryPermission::DEVELOPER_PASS),
|
||||
);
|
||||
ctx.insert("home", &ua.settings.default_timeline.relative_url());
|
||||
} else {
|
||||
ctx.insert("is_helper", &false);
|
||||
|
|
|
@ -9,7 +9,10 @@ mod sanitize;
|
|||
|
||||
use assets::{init_dirs, write_assets};
|
||||
use stripe::Client as StripeClient;
|
||||
use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji};
|
||||
use tetratto_core::model::{
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
uploads::CustomEmoji,
|
||||
};
|
||||
pub use tetratto_core::*;
|
||||
|
||||
use axum::{
|
||||
|
@ -55,6 +58,15 @@ fn check_supporter(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Va
|
|||
.into())
|
||||
}
|
||||
|
||||
fn check_dev_pass(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
Ok(
|
||||
SecondaryPermission::from_bits(value.as_u64().unwrap() as u32)
|
||||
.unwrap()
|
||||
.check(SecondaryPermission::DEVELOPER_PASS)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn check_staff_badge(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
Ok(FinePermission::from_bits(value.as_u64().unwrap() as u32)
|
||||
.unwrap()
|
||||
|
@ -109,6 +121,7 @@ async fn main() {
|
|||
tera.register_filter("markdown", render_markdown);
|
||||
tera.register_filter("color", color_escape);
|
||||
tera.register_filter("has_supporter", check_supporter);
|
||||
tera.register_filter("has_dev_pass", check_dev_pass);
|
||||
tera.register_filter("has_staff_badge", check_staff_badge);
|
||||
tera.register_filter("has_banned", check_banned);
|
||||
tera.register_filter("remove_script_tags", remove_script_tags);
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
("class" "hidden lowered card w-full no_p_margin")
|
||||
("ui_ident" "purchase_help")
|
||||
(b (text "What does \"Purchase account\" mean?"))
|
||||
(p (text "Your account will be created, but you cannot use it until you activate it for {{ config.stripe.supporter_price_text }}."))
|
||||
(p (text "Your account will be created, but you cannot use it until you activate it for {{ config.stripe.price_texts.supporter }}."))
|
||||
(p (text "Alternatively, you can provide an invite code to create your account for free.")))
|
||||
(text "{%- endif %}")
|
||||
(button
|
||||
|
|
|
@ -1452,7 +1452,9 @@
|
|||
});
|
||||
})();"))
|
||||
|
||||
(text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}")
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}")
|
||||
(div
|
||||
("class" "card w-full supporter_ad")
|
||||
("ui_ident" "supporter_ad")
|
||||
|
@ -1472,8 +1474,9 @@
|
|||
(text "{{ icon \"heart\" }}")
|
||||
(span
|
||||
(text "{{ text \"general:action.become_supporter\" }}")))))
|
||||
(text "{%- endif %} {%- endmacro %}")
|
||||
|
||||
(text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}")
|
||||
(text "{% macro create_post_options() -%}")
|
||||
(div
|
||||
("class" "flex gap-2 flex-wrap")
|
||||
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}")
|
||||
|
@ -2358,10 +2361,6 @@
|
|||
(text "Save infinite post drafts"))
|
||||
(li
|
||||
(text "Ability to search through all posts"))
|
||||
(li
|
||||
(text "Ability to create forges"))
|
||||
(li
|
||||
(text "Create more than 1 app"))
|
||||
(li
|
||||
(text "Create up to 10 stack blocks"))
|
||||
(li
|
||||
|
@ -2388,15 +2387,13 @@
|
|||
("href" "{{ config.stripe.payment_links.supporter }}?client_reference_id={{ user.id }}")
|
||||
("class" "button")
|
||||
("target" "_blank")
|
||||
(text "Become a supporter ({{ config.stripe.supporter_price_text }})"))
|
||||
(text "Become a supporter ({{ config.stripe.price_texts.supporter }})"))
|
||||
(span
|
||||
("class" "fade")
|
||||
(text "Please use your")
|
||||
(b
|
||||
(text " real email "))
|
||||
(text "when
|
||||
completing payment. It is required to manage
|
||||
your billing settings."))
|
||||
(text "when completing payment. It is required to manage your billing settings."))
|
||||
|
||||
(text "{% if config.security.enable_invite_codes -%}")
|
||||
(span
|
||||
|
@ -2405,3 +2402,46 @@
|
|||
(b (text "1: ")) (text "After your account is at least 1 month old"))
|
||||
(text "{%- endif %}")
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro get_developer_pass_button() -%}")
|
||||
(p
|
||||
(text "You currently do not hold a developer pass. With a developer pass, you'll get:"))
|
||||
(ul
|
||||
("style" "margin-bottom: var(--pad-4)")
|
||||
(li
|
||||
(text "Increased app storage limit (500 KB->5 MB)"))
|
||||
(li
|
||||
(text "Ability to create forges"))
|
||||
(li
|
||||
(text "Ability to create more than 1 app"))
|
||||
(li
|
||||
(text "Developer pass profile badge")))
|
||||
(a
|
||||
("href" "{{ config.stripe.payment_links.dev_pass }}?client_reference_id={{ user.id }}")
|
||||
("class" "button")
|
||||
("target" "_blank")
|
||||
(text "Continue ({{ config.stripe.price_texts.dev_pass }})"))
|
||||
(span
|
||||
("class" "fade")
|
||||
(text "Please use your")
|
||||
(b
|
||||
(text " real email "))
|
||||
(text "when completing payment. It is required to manage your billing settings. If you're already a supporter, please use the same email you used there."))
|
||||
(text "{%- endmacro %}")
|
||||
|
||||
(text "{% macro developer_pass_ad(body) -%} {% if config.stripe and not has_developer_pass %}")
|
||||
(div
|
||||
("class" "card w-full supporter_ad")
|
||||
("ui_ident" "supporter_ad")
|
||||
("onclick" "window.location.href = '/settings#/account/billing'")
|
||||
(div
|
||||
("class" "card w-full flex flex-wrap items-center gap-2 justify-between")
|
||||
(b
|
||||
(text "{{ body }}"))
|
||||
(a
|
||||
("href" "/settings#/account/billing")
|
||||
("class" "button small")
|
||||
(icon (text "arrow-right"))
|
||||
(span
|
||||
(str (text "dialog:action.continue"))))))
|
||||
(text "{%- endif %} {%- endmacro %}")
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
(main
|
||||
("class" "flex flex-col gap-2")
|
||||
; create new
|
||||
(text "{% if user.permissions|has_supporter -%}")
|
||||
(text "{% if user.secondary_permissions|has_dev_pass -%}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(div
|
||||
|
@ -32,7 +32,7 @@
|
|||
(button
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
(text "{% else %}")
|
||||
(text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}")
|
||||
(text "{{ components::developer_pass_ad(body=\"Get a developer pass to create forges!\") }}")
|
||||
(text "{%- endif %}")
|
||||
|
||||
; forge listing
|
||||
|
|
|
@ -72,19 +72,25 @@
|
|||
("style" "color: var(--color-primary);")
|
||||
("class" "flex items-center")
|
||||
(text "{{ icon \"badge-check\" }}"))
|
||||
(text "{%- endif %} {% if profile.permissions|has_supporter -%}")
|
||||
(text "{%- endif %} {% if profile.permissions|has_supporter -%}")
|
||||
(span
|
||||
("title" "Supporter")
|
||||
("style" "color: var(--color-primary);")
|
||||
("class" "flex items-center")
|
||||
(text "{{ icon \"star\" }}"))
|
||||
(text "{%- endif %} {% if profile.permissions|has_staff_badge -%}")
|
||||
(text "{%- endif %} {% if profile.secondary_permissions|has_dev_pass -%}")
|
||||
(span
|
||||
("title" "Developer pass")
|
||||
("style" "color: var(--color-primary);")
|
||||
("class" "flex items-center")
|
||||
(text "{{ icon \"id-card-lanyard\" }}"))
|
||||
(text "{%- endif %} {% if profile.permissions|has_staff_badge -%}")
|
||||
(span
|
||||
("title" "Staff")
|
||||
("style" "color: var(--color-primary);")
|
||||
("class" "flex items-center")
|
||||
(text "{{ icon \"shield-user\" }}"))
|
||||
(text "{%- endif %} {% if profile.permissions|has_banned -%}")
|
||||
(text "{%- endif %} {% if profile.permissions|has_banned -%}")
|
||||
(span
|
||||
("title" "Banned")
|
||||
("style" "color: var(--color-primary);")
|
||||
|
|
|
@ -823,6 +823,29 @@
|
|||
(div
|
||||
("class" "card flex flex-col gap-2 secondary")
|
||||
(text "{% if config.stripe -%}")
|
||||
(text "{% if has_developer_pass or is_supporter -%}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
("ui_ident" "supporter_card")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "credit-card"))
|
||||
(b
|
||||
(text "Manage billing")))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
(p
|
||||
(text "You currently have a subscription! You can manage your billing information below. ")
|
||||
(b
|
||||
(text "Please use your email address you supplied when paying to log into the billing portal."))
|
||||
(text " You can manage all of your active subscriptions through this page."))
|
||||
(a
|
||||
("href" "{{ config.stripe.billing_portal_url }}")
|
||||
("class" "button lowered")
|
||||
("target" "_blank")
|
||||
(text "Manage billing"))))
|
||||
(text "{%- endif %}")
|
||||
|
||||
(div
|
||||
("class" "card-nest")
|
||||
("ui_ident" "supporter_card")
|
||||
|
@ -832,28 +855,33 @@
|
|||
(b
|
||||
(text "Supporter status")))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2")
|
||||
("class" "card flex flex-col gap-2 no_p_margin")
|
||||
(text "{% if is_supporter -%}")
|
||||
(p
|
||||
(text "You ")
|
||||
(b
|
||||
(text "are "))
|
||||
(text "a supporter! Thank you for all
|
||||
that you do. You can manage your billing
|
||||
information below.")
|
||||
(b
|
||||
(text "Please use your email address you supplied
|
||||
when paying to login to the billing
|
||||
portal.")))
|
||||
(a
|
||||
("href" "{{ config.stripe.billing_portal_url }}")
|
||||
("class" "button lowered")
|
||||
("target" "_blank")
|
||||
(text "Manage billing"))
|
||||
(b (text "are "))
|
||||
(text "a supporter! Thank you for all that you do."))
|
||||
(text "{% else %}")
|
||||
(text "{{ components::become_supporter_button() }}")
|
||||
(text "{%- endif %}")))
|
||||
|
||||
(div
|
||||
("class" "card-nest")
|
||||
("ui_ident" "supporter_card")
|
||||
(div
|
||||
("class" "card small flex items-center gap-2")
|
||||
(icon (text "id-card-lanyard"))
|
||||
(b
|
||||
(text "Developer pass status")))
|
||||
(div
|
||||
("class" "card flex flex-col gap-2 no_p_margin")
|
||||
(text "{% if has_developer_pass -%}")
|
||||
(p
|
||||
(text "You currently have a developer pass!"))
|
||||
(text "{% else %}")
|
||||
(text "{{ components::get_developer_pass_button() }}")
|
||||
(text "{%- endif %}")))
|
||||
|
||||
(text "{% if user.was_purchased and user.invite_code == 0 -%}")
|
||||
(form
|
||||
("class" "card w-full lowered flex flex-col gap-2")
|
||||
|
|
|
@ -3,9 +3,9 @@ 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},
|
||||
auth::{Notification, User},
|
||||
moderation::AuditLogEntry,
|
||||
permissions::FinePermission,
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
ApiReturn, Error,
|
||||
};
|
||||
use stripe::{EventObject, EventType};
|
||||
|
@ -205,6 +205,43 @@ pub async fn stripe_webhook(
|
|||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
} else if product_id == stripe_cnf.product_ids.dev_pass {
|
||||
// dev pass
|
||||
tracing::info!("found subscription user in {retries} tries");
|
||||
|
||||
if user
|
||||
.secondary_permissions
|
||||
.check(SecondaryPermission::DEVELOPER_PASS)
|
||||
{
|
||||
return Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Already applied".to_string(),
|
||||
payload: (),
|
||||
});
|
||||
}
|
||||
|
||||
tracing::info!("invoice {} (stripe: {})", user.id, customer_id);
|
||||
let new_user_permissions =
|
||||
user.secondary_permissions | SecondaryPermission::DEVELOPER_PASS;
|
||||
|
||||
if let Err(e) = data
|
||||
.update_user_secondary_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 developer!".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"
|
||||
|
@ -220,34 +257,72 @@ pub async fn stripe_webhook(
|
|||
};
|
||||
|
||||
let customer_id = subscription.customer.id();
|
||||
let product_id = subscription
|
||||
.items
|
||||
.data
|
||||
.get(0)
|
||||
.as_ref()
|
||||
.expect("cancelled nothing?")
|
||||
.plan
|
||||
.as_ref()
|
||||
.expect("no subscription plan?")
|
||||
.product
|
||||
.as_ref()
|
||||
.expect("plan with no product?")
|
||||
.id()
|
||||
.to_string();
|
||||
|
||||
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;
|
||||
// handle each subscription item
|
||||
if product_id == stripe_cnf.product_ids.supporter {
|
||||
// supporter
|
||||
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)
|
||||
.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());
|
||||
}
|
||||
}
|
||||
} else if product_id == stripe_cnf.product_ids.dev_pass {
|
||||
// dev pass
|
||||
tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id);
|
||||
let new_user_permissions =
|
||||
user.secondary_permissions - SecondaryPermission::DEVELOPER_PASS;
|
||||
|
||||
if let Err(e) = data
|
||||
.update_user_secondary_role(user.id, new_user_permissions, user.clone(), true)
|
||||
.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());
|
||||
}
|
||||
|
||||
// send notification
|
||||
if let Err(e) = data
|
||||
.create_notification(Notification::new(
|
||||
"Sorry to see you go... :(".to_string(),
|
||||
|
@ -269,46 +344,112 @@ pub async fn stripe_webhook(
|
|||
|
||||
let customer_id = invoice.customer.expect("TETRATTO_STRIPE_NO_CUSTOMER").id();
|
||||
|
||||
let item = match invoice.lines.as_ref().expect("no line items?").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();
|
||||
|
||||
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: (),
|
||||
});
|
||||
}
|
||||
// handle each subscription item
|
||||
if product_id == stripe_cnf.product_ids.supporter {
|
||||
// supporter
|
||||
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;
|
||||
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)
|
||||
.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());
|
||||
}
|
||||
}
|
||||
} else if product_id == stripe_cnf.product_ids.dev_pass {
|
||||
// dev pass
|
||||
if !user
|
||||
.secondary_permissions
|
||||
.check(SecondaryPermission::DEVELOPER_PASS)
|
||||
{
|
||||
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.secondary_permissions - SecondaryPermission::DEVELOPER_PASS;
|
||||
|
||||
if let Err(e) = data
|
||||
.update_user_secondary_role(user.id, new_user_permissions, user.clone(), true)
|
||||
.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());
|
||||
}
|
||||
|
||||
// send notification
|
||||
if let Err(e) = data
|
||||
.create_notification(Notification::new(
|
||||
"It seems your recent payment has failed :(".to_string(),
|
||||
|
|
|
@ -194,22 +194,30 @@ pub struct StripeConfig {
|
|||
///
|
||||
/// <https://docs.stripe.com/no-code/customer-portal>
|
||||
pub billing_portal_url: String,
|
||||
/// The text representation of the price of supporter. (like `$4 USD`)
|
||||
pub supporter_price_text: String,
|
||||
/// The text representation of prices. (like `$4 USD`)
|
||||
pub price_texts: StripePriceTexts,
|
||||
/// Product IDs from the Stripe dashboard.
|
||||
///
|
||||
/// These are checked when we receive a webhook to ensure we provide the correct product.
|
||||
pub product_ids: StripeProductIds,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct StripePriceTexts {
|
||||
pub supporter: String,
|
||||
pub dev_pass: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct StripePaymentLinks {
|
||||
pub supporter: String,
|
||||
pub dev_pass: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct StripeProductIds {
|
||||
pub supporter: String,
|
||||
pub dev_pass: String,
|
||||
}
|
||||
|
||||
/// Manuals config (search help, etc)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use oiseau::cache::Cache;
|
||||
use crate::model::apps::{AppDataQuery, AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery};
|
||||
use crate::model::apps::{AppDataQuery, AppDataQueryResult, AppDataSelectMode};
|
||||
use crate::model::{apps::AppData, permissions::FinePermission, Error, Result};
|
||||
use crate::{auto_method, DataManager};
|
||||
use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
|
||||
|
@ -51,13 +51,7 @@ impl DataManager {
|
|||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let query_str = query.to_string().replace(
|
||||
"%q%",
|
||||
&match query.query {
|
||||
AppDataSelectQuery::KeyIs(_) => format!("k = $1"),
|
||||
AppDataSelectQuery::LikeJson(_, _) => format!("v LIKE $1"),
|
||||
},
|
||||
);
|
||||
let query_str = query.to_string().replace("%q%", &query.query.selector());
|
||||
|
||||
let res = match query.mode {
|
||||
AppDataSelectMode::One(_) => AppDataQueryResult::One(
|
||||
|
@ -98,13 +92,7 @@ impl DataManager {
|
|||
|
||||
let query_str = query
|
||||
.to_string()
|
||||
.replace(
|
||||
"%q%",
|
||||
&match query.query {
|
||||
AppDataSelectQuery::KeyIs(_) => format!("k = $1"),
|
||||
AppDataSelectQuery::LikeJson(_, _) => format!("v LIKE $1"),
|
||||
},
|
||||
)
|
||||
.replace("%q%", &query.query.selector())
|
||||
.replace("SELECT * FROM", "SELECT id FROM");
|
||||
|
||||
if let Err(e) = execute!(
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::model::{
|
|||
apps::{AppQuota, ThirdPartyApp},
|
||||
auth::User,
|
||||
oauth::AppScope,
|
||||
permissions::FinePermission,
|
||||
permissions::{FinePermission, SecondaryPermission},
|
||||
Error, Result,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
|
@ -72,10 +72,15 @@ impl DataManager {
|
|||
// check number of apps
|
||||
let owner = self.get_user_by_id(data.owner).await?;
|
||||
|
||||
if !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||
let apps = self.get_apps_by_owner(data.owner).await?;
|
||||
if !owner
|
||||
.secondary_permissions
|
||||
.check(SecondaryPermission::DEVELOPER_PASS)
|
||||
{
|
||||
let apps = self
|
||||
.get_table_row_count_where("apps", &format!("owner = {}", owner.id))
|
||||
.await? as usize;
|
||||
|
||||
if apps.len() >= Self::MAXIMUM_FREE_APPS {
|
||||
if apps >= Self::MAXIMUM_FREE_APPS {
|
||||
return Err(Error::MiscError(
|
||||
"You already have the maximum number of apps you can have".to_string(),
|
||||
));
|
||||
|
|
|
@ -85,7 +85,7 @@ impl DataManager {
|
|||
|
||||
let res = query_row!(
|
||||
&conn,
|
||||
&format!("SELECT COUNT(*)::int FROM {} {}", table, r#where),
|
||||
&format!("SELECT COUNT(*)::int FROM {} WHERE {}", table, r#where),
|
||||
params![],
|
||||
|x| Ok(x.get::<usize, i32>(0))
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ use super::common::NAME_REGEX;
|
|||
use oiseau::cache::Cache;
|
||||
use crate::model::communities::{CommunityContext, CommunityJoinAccess, CommunityMembership};
|
||||
use crate::model::communities_permissions::CommunityPermission;
|
||||
use crate::model::permissions::SecondaryPermission;
|
||||
use crate::model::{
|
||||
Error, Result,
|
||||
auth::User,
|
||||
|
@ -255,7 +256,11 @@ impl DataManager {
|
|||
|
||||
// check is_forge
|
||||
// only supporters can CREATE forge communities... anybody can contribute to them
|
||||
if data.is_forge && !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||
if data.is_forge
|
||||
&& !owner
|
||||
.secondary_permissions
|
||||
.check(SecondaryPermission::DEVELOPER_PASS)
|
||||
{
|
||||
return Err(Error::RequiresSupporter);
|
||||
}
|
||||
|
||||
|
|
|
@ -147,6 +147,8 @@ impl AppData {
|
|||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum AppDataSelectQuery {
|
||||
KeyIs(String),
|
||||
KeyLike(String),
|
||||
ValueLike(String),
|
||||
LikeJson(String, String),
|
||||
}
|
||||
|
||||
|
@ -154,11 +156,24 @@ impl Display for AppDataSelectQuery {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&match self {
|
||||
Self::KeyIs(k) => k.to_owned(),
|
||||
Self::KeyLike(k) => k.to_owned(),
|
||||
Self::ValueLike(v) => v.to_owned(),
|
||||
Self::LikeJson(k, v) => format!("%\"{k}\":\"{v}\"%"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AppDataSelectQuery {
|
||||
pub fn selector(&self) -> String {
|
||||
match self {
|
||||
AppDataSelectQuery::KeyIs(_) => format!("k = $1"),
|
||||
AppDataSelectQuery::KeyLike(_) => format!("k LIKE $1"),
|
||||
AppDataSelectQuery::ValueLike(_) => format!("v LIKE $1"),
|
||||
AppDataSelectQuery::LikeJson(_, _) => format!("v LIKE $1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum AppDataSelectMode {
|
||||
/// Select a single row (with offset).
|
||||
|
@ -179,14 +194,14 @@ impl Display for AppDataSelectMode {
|
|||
Self::One(offset) => format!("LIMIT 1 OFFSET {offset}"),
|
||||
Self::Many(limit, offset) => {
|
||||
format!(
|
||||
"LIMIT {} OFFSET {offset}",
|
||||
if *limit > 1024 { 1024 } else { *limit }
|
||||
"ORDER BY k DESC LIMIT {} OFFSET {offset}",
|
||||
if *limit > 24 { 24 } else { *limit }
|
||||
)
|
||||
}
|
||||
Self::ManyJson(order_by_top_level_key, limit, offset) => {
|
||||
format!(
|
||||
"ORDER BY v::jsonb->>'{order_by_top_level_key}' DESC LIMIT {} OFFSET {offset}",
|
||||
if *limit > 1024 { 1024 } else { *limit }
|
||||
if *limit > 24 { 24 } else { *limit }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue