diff --git a/backend/spec/api.md b/backend/spec/api.md index 872d0a2..35d09dc 100644 --- a/backend/spec/api.md +++ b/backend/spec/api.md @@ -204,7 +204,8 @@ Refer to https://github.com/nostr-protocol/nips/blob/master/98.md for details. U ## `prepare_relay(&self, relay: Relay) -> anyhow::Result` - Validate `subdomain` -- If `plan` is free and `blossom` is enabled, return `premium-feature` -- If `plan` is free and `livekit` is enabled, return `premium-feature` +- Validate that `plan` matches a known plan id from `Query::list_plans` +- If selected `plan` does not include `blossom` and `blossom` is enabled, return `premium-feature` +- If selected `plan` does not include `livekit` and `livekit` is enabled, return `premium-feature` - Populate `schema` if not already set - Populate missing fields using reasonable defaults diff --git a/backend/src/api.rs b/backend/src/api.rs index ec98a46..34ae5fd 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -259,10 +259,12 @@ impl Api { return Err(anyhow!("invalid-subdomain")); } - if relay.plan == "free" && relay.blossom_enabled == 1 { + let plan = Query::get_plan(&relay.plan).ok_or_else(|| anyhow!("invalid-plan"))?; + + if !plan.blossom && relay.blossom_enabled == 1 { return Err(anyhow!("premium-feature")); } - if relay.plan == "free" && relay.livekit_enabled == 1 { + if !plan.livekit && relay.livekit_enabled == 1 { return Err(anyhow!("premium-feature")); } @@ -276,14 +278,10 @@ impl Api { relay.policy_strip_signatures = parse_bool_default(relay.policy_strip_signatures, 0); relay.groups_enabled = parse_bool_default(relay.groups_enabled, 1); relay.management_enabled = parse_bool_default(relay.management_enabled, 1); - relay.blossom_enabled = parse_bool_default( - relay.blossom_enabled, - if relay.plan == "free" { 0 } else { 1 }, - ); - relay.livekit_enabled = parse_bool_default( - relay.livekit_enabled, - if relay.plan == "free" { 0 } else { 1 }, - ); + relay.blossom_enabled = + parse_bool_default(relay.blossom_enabled, if plan.blossom { 1 } else { 0 }); + relay.livekit_enabled = + parse_bool_default(relay.livekit_enabled, if plan.livekit { 1 } else { 0 }); relay.push_enabled = parse_bool_default(relay.push_enabled, 1); Ok(relay) @@ -453,7 +451,7 @@ async fn get_identity( } async fn get_plan(Path(id): Path) -> Response { - match Query::list_plans().into_iter().find(|p| p.id == id) { + match Query::get_plan(&id) { Some(plan) => ok(StatusCode::OK, plan), None => err(StatusCode::NOT_FOUND, "not-found", "plan not found"), } @@ -594,6 +592,13 @@ async fn create_relay( }; relay = match state.api.prepare_relay(relay) { + Err(e) if e.to_string() == "invalid-plan" => { + return Ok(err( + StatusCode::UNPROCESSABLE_ENTITY, + "invalid-plan", + "plan not found", + )); + } Ok(r) => r, Err(e) if e.to_string() == "premium-feature" => { return Ok(err( @@ -691,6 +696,13 @@ async fn update_relay( } relay = match state.api.prepare_relay(relay) { + Err(e) if e.to_string() == "invalid-plan" => { + return Ok(err( + StatusCode::UNPROCESSABLE_ENTITY, + "invalid-plan", + "plan not found", + )); + } Ok(r) => r, Err(e) if e.to_string() == "premium-feature" => { return Ok(err( diff --git a/backend/src/billing.rs b/backend/src/billing.rs index 8a41265..997f0f0 100644 --- a/backend/src/billing.rs +++ b/backend/src/billing.rs @@ -154,8 +154,11 @@ impl Billing { return Ok(()); }; + let plan = Query::get_plan(&relay.plan) + .ok_or_else(|| anyhow!("unknown relay plan id: {}", relay.plan))?; + // Free plan: remove subscription item if exists, then clean up - if relay.plan == "free" { + if plan.id == "free" { if let Some(ref item_id) = relay.stripe_subscription_item_id { self.stripe_delete_subscription_item(item_id).await?; self.command @@ -179,12 +182,6 @@ impl Billing { } // Active relay on a paid plan - let plan = Query::list_plans().into_iter().find(|p| p.id == relay.plan); - - let Some(plan) = plan else { - return Ok(()); - }; - let Some(ref stripe_price_id) = plan.stripe_price_id else { return Ok(()); }; @@ -442,7 +439,7 @@ impl Billing { let relays = self.query.list_relays_for_tenant(&tenant.pubkey).await?; for relay in relays { - if relay.status == RELAY_STATUS_ACTIVE && relay.plan != "free" { + if relay.status == RELAY_STATUS_ACTIVE && Query::is_paid_plan(&relay.plan) { self.command.mark_relay_delinquent(&relay).await?; } } @@ -477,7 +474,7 @@ impl Billing { let relays = self.query.list_relays_for_tenant(&tenant.pubkey).await?; for relay in relays { - if relay.status == RELAY_STATUS_ACTIVE && relay.plan != "free" { + if relay.status == RELAY_STATUS_ACTIVE && Query::is_paid_plan(&relay.plan) { self.command.mark_relay_delinquent(&relay).await?; } } @@ -801,7 +798,7 @@ impl Billing { } fn should_reactivate_after_payment(relay: &Relay) -> bool { - relay.status == RELAY_STATUS_DELINQUENT && relay.plan != "free" + relay.status == RELAY_STATUS_DELINQUENT && Query::is_paid_plan(&relay.plan) } async fn fetch_btc_spot_price(&self, currency: &str) -> Result { diff --git a/backend/src/query.rs b/backend/src/query.rs index b596b45..7cfd77b 100644 --- a/backend/src/query.rs +++ b/backend/src/query.rs @@ -68,6 +68,16 @@ impl Query { ] } + pub fn get_plan(plan_id: &str) -> Option { + Self::list_plans().into_iter().find(|p| p.id == plan_id) + } + + pub fn is_paid_plan(plan_id: &str) -> bool { + Self::get_plan(plan_id) + .map(|p| p.id != "free") + .unwrap_or(false) + } + pub async fn list_relays(&self) -> Result> { let rows = sqlx::query_as::<_, Relay>( "SELECT id, tenant, schema, subdomain, plan, stripe_subscription_item_id, @@ -135,13 +145,14 @@ impl Query { } pub async fn has_active_paid_relays(&self, tenant_id: &str) -> Result { - let count = sqlx::query_scalar::<_, i64>( - "SELECT COUNT(*) FROM relay WHERE tenant = ? AND status = 'active' AND plan != 'free'", + let plans = sqlx::query_scalar::<_, String>( + "SELECT plan FROM relay WHERE tenant = ? AND status = 'active'", ) .bind(tenant_id) - .fetch_one(&self.pool) + .fetch_all(&self.pool) .await?; - Ok(count > 0) + + Ok(plans.into_iter().any(|plan| Self::is_paid_plan(&plan))) } pub async fn list_activity_for_relay(&self, relay_id: &str) -> Result> {