Rust things
This commit is contained in:
parent
fc10381692
commit
3debacab4f
30 changed files with 3257 additions and 647 deletions
|
|
@ -9,11 +9,16 @@ use serde::{Deserialize, Serialize};
|
|||
use tracing::{info, warn};
|
||||
|
||||
use crate::auth::{OptionalUser, PocketBaseUser};
|
||||
use crate::checkout_sessions::{
|
||||
active_referral_checkout_user, start_license_checkout, CheckoutStart,
|
||||
};
|
||||
use crate::pocketbase::get_superuser_token;
|
||||
use crate::pocketbase_locks::acquire_pocketbase_lock;
|
||||
use crate::state::{AppState, SharedState};
|
||||
|
||||
static INVITE_REDEMPTIONS_IN_PROGRESS: LazyLock<Mutex<HashSet<String>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
const INVITE_REDEMPTION_LOCK_TTL_SECS: u64 = 5 * 60;
|
||||
|
||||
struct InviteRedemptionGuard {
|
||||
code: String,
|
||||
|
|
@ -103,7 +108,7 @@ fn validate_invite_code(code: &str) -> Result<(), &'static str> {
|
|||
}
|
||||
|
||||
fn generate_invite_code() -> String {
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
let mut rng = rand::rng();
|
||||
let chars: Vec<char> = (0..12)
|
||||
.map(|_| {
|
||||
|
|
@ -246,74 +251,26 @@ async fn grant_license_for_invite(
|
|||
async fn create_referral_checkout(
|
||||
state: &AppState,
|
||||
user: &PocketBaseUser,
|
||||
invite_id: &str,
|
||||
) -> Result<String, Response> {
|
||||
let count = match super::pricing::count_licensed_users(state).await {
|
||||
Ok(count) => count,
|
||||
Err(err) => {
|
||||
warn!("Failed to count licensed users for invite checkout: {err}");
|
||||
return Err(StatusCode::SERVICE_UNAVAILABLE.into_response());
|
||||
}
|
||||
};
|
||||
let price_pence = super::pricing::price_for_count(count);
|
||||
|
||||
let public_url = &state.public_url;
|
||||
let success_url = format!("{public_url}/pricing?license_success=1");
|
||||
let cancel_url = format!("{public_url}/pricing");
|
||||
|
||||
let form_params = vec![
|
||||
("mode", "payment".to_string()),
|
||||
(
|
||||
"line_items[0][price_data][unit_amount]",
|
||||
price_pence.to_string(),
|
||||
),
|
||||
("line_items[0][price_data][currency]", "gbp".to_string()),
|
||||
(
|
||||
"line_items[0][price_data][product_data][name]",
|
||||
"Perfect Postcodes Lifetime License".to_string(),
|
||||
),
|
||||
("line_items[0][quantity]", "1".to_string()),
|
||||
("success_url", success_url),
|
||||
("cancel_url", cancel_url),
|
||||
("client_reference_id", user.id.clone()),
|
||||
("customer_email", user.email.clone()),
|
||||
(
|
||||
"discounts[0][coupon]",
|
||||
state.stripe_referral_coupon_id.clone(),
|
||||
),
|
||||
];
|
||||
|
||||
let stripe_res = state
|
||||
.http_client
|
||||
.post("https://api.stripe.com/v1/checkout/sessions")
|
||||
.basic_auth(&state.stripe_secret_key, None::<&str>)
|
||||
.form(&form_params)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match stripe_res {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let stripe_body: serde_json::Value = match resp.json().await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
warn!("Failed to parse Stripe checkout response: {err}");
|
||||
return Err(StatusCode::BAD_GATEWAY.into_response());
|
||||
}
|
||||
};
|
||||
let checkout_url = stripe_body["url"].as_str().unwrap_or_default().to_string();
|
||||
if checkout_url.is_empty() {
|
||||
warn!("Stripe checkout response did not include a URL");
|
||||
return Err(StatusCode::BAD_GATEWAY.into_response());
|
||||
}
|
||||
Ok(checkout_url)
|
||||
}
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
warn!("Failed to create Stripe checkout for referral invite ({status}): {text}");
|
||||
Err(StatusCode::BAD_GATEWAY.into_response())
|
||||
}
|
||||
match start_license_checkout(
|
||||
state,
|
||||
user,
|
||||
&success_url,
|
||||
&cancel_url,
|
||||
Some(&state.stripe_referral_coupon_id),
|
||||
Some(invite_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(CheckoutStart::Free) => Ok(success_url),
|
||||
Ok(CheckoutStart::Stripe { url }) => Ok(url),
|
||||
Err(err) => {
|
||||
warn!("Stripe request error for referral invite: {err}");
|
||||
warn!("Failed to create reserved Stripe checkout for referral invite: {err:?}");
|
||||
Err(StatusCode::BAD_GATEWAY.into_response())
|
||||
}
|
||||
}
|
||||
|
|
@ -541,6 +498,10 @@ pub async fn post_redeem_invite(
|
|||
.into_response();
|
||||
}
|
||||
|
||||
if user.is_admin || user.subscription == "licensed" {
|
||||
return (StatusCode::CONFLICT, "Account already has full access").into_response();
|
||||
}
|
||||
|
||||
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
||||
|
||||
let token = match get_superuser_token(&state).await {
|
||||
|
|
@ -561,6 +522,19 @@ pub async fn post_redeem_invite(
|
|||
.into_response()
|
||||
}
|
||||
};
|
||||
let lock_name = format!("invite:{}", req.code);
|
||||
let _distributed_redemption_guard =
|
||||
match acquire_pocketbase_lock(&state, &lock_name, INVITE_REDEMPTION_LOCK_TTL_SECS).await {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
warn!(code = %req.code, "Failed to acquire invite redemption lock: {err}");
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
"Invite redemption is already in progress",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let invite = match lookup_unused_invite(&state, pb_url, &token, &req.code).await {
|
||||
Ok(Some(invite)) => invite,
|
||||
|
|
@ -591,11 +565,11 @@ pub async fn post_redeem_invite(
|
|||
};
|
||||
|
||||
if invite_type == "admin" {
|
||||
if let Err(response) = grant_license_for_invite(&state, pb_url, &token, &user.id).await {
|
||||
if let Err(response) = mark_invite_used(&state, pb_url, &token, invite_id, &user.id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
if let Err(response) = mark_invite_used(&state, pb_url, &token, invite_id, &user.id).await {
|
||||
if let Err(response) = grant_license_for_invite(&state, pb_url, &token, &user.id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
@ -607,15 +581,26 @@ pub async fn post_redeem_invite(
|
|||
.into_response();
|
||||
}
|
||||
|
||||
let checkout_url = match create_referral_checkout(&state, &user).await {
|
||||
match active_referral_checkout_user(&state, invite_id).await {
|
||||
Ok(Some(active_user_id)) if active_user_id != user.id => {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
"Invite checkout is already in progress",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
warn!(code = %req.code, "Failed to check active referral checkout: {err}");
|
||||
return StatusCode::BAD_GATEWAY.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let checkout_url = match create_referral_checkout(&state, &user, invite_id).await {
|
||||
Ok(url) => url,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
if let Err(response) = mark_invite_used(&state, pb_url, &token, invite_id, &user.id).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
info!(user_id = %user.id, code = %req.code, "Referral invite redeemed; checkout created");
|
||||
Json(RedeemResponse {
|
||||
result: "checkout".to_string(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue