Rust things

This commit is contained in:
Andras Schmelczer 2026-05-10 14:55:43 +01:00
parent fc10381692
commit 3debacab4f
30 changed files with 3257 additions and 647 deletions

View file

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