Rename tenant fields to tenant_pubkey and plan to plan_id
This commit is contained in:
@@ -1,14 +1,3 @@
|
|||||||
CREATE TABLE IF NOT EXISTS activity (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
tenant TEXT NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
activity_type TEXT NOT NULL,
|
|
||||||
resource_type TEXT NOT NULL,
|
|
||||||
resource_id TEXT NOT NULL,
|
|
||||||
billed_at INTEGER,
|
|
||||||
plan_id TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tenant (
|
CREATE TABLE IF NOT EXISTS tenant (
|
||||||
pubkey TEXT PRIMARY KEY,
|
pubkey TEXT PRIMARY KEY,
|
||||||
nwc_url TEXT NOT NULL DEFAULT '',
|
nwc_url TEXT NOT NULL DEFAULT '',
|
||||||
@@ -19,11 +8,24 @@ CREATE TABLE IF NOT EXISTS tenant (
|
|||||||
renewed_at INTEGER
|
renewed_at INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS activity (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tenant_pubkey TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
activity_type TEXT NOT NULL,
|
||||||
|
resource_type TEXT NOT NULL,
|
||||||
|
resource_id TEXT NOT NULL,
|
||||||
|
billed_at INTEGER,
|
||||||
|
plan_id TEXT,
|
||||||
|
FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS relay (
|
CREATE TABLE IF NOT EXISTS relay (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
tenant TEXT NOT NULL,
|
tenant_pubkey TEXT NOT NULL,
|
||||||
subdomain TEXT NOT NULL UNIQUE,
|
subdomain TEXT NOT NULL UNIQUE,
|
||||||
plan TEXT NOT NULL,
|
plan_id TEXT NOT NULL,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
synced INTEGER NOT NULL DEFAULT 0,
|
synced INTEGER NOT NULL DEFAULT 0,
|
||||||
sync_error TEXT NOT NULL DEFAULT '',
|
sync_error TEXT NOT NULL DEFAULT '',
|
||||||
@@ -37,7 +39,7 @@ CREATE TABLE IF NOT EXISTS relay (
|
|||||||
blossom_enabled INTEGER NOT NULL DEFAULT 0,
|
blossom_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
livekit_enabled INTEGER NOT NULL DEFAULT 0,
|
livekit_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
push_enabled INTEGER NOT NULL DEFAULT 1,
|
push_enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
FOREIGN KEY (tenant) REFERENCES tenant(pubkey)
|
FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS invoice (
|
CREATE TABLE IF NOT EXISTS invoice (
|
||||||
@@ -58,7 +60,7 @@ CREATE TABLE IF NOT EXISTS invoice_item (
|
|||||||
activity_id TEXT,
|
activity_id TEXT,
|
||||||
tenant_pubkey TEXT NOT NULL,
|
tenant_pubkey TEXT NOT NULL,
|
||||||
relay_id TEXT NOT NULL,
|
relay_id TEXT NOT NULL,
|
||||||
plan TEXT NOT NULL,
|
plan_id TEXT NOT NULL,
|
||||||
amount INTEGER NOT NULL,
|
amount INTEGER NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
@@ -84,13 +86,13 @@ CREATE TABLE IF NOT EXISTS intent (
|
|||||||
FOREIGN KEY (invoice_id) REFERENCES invoice(id)
|
FOREIGN KEY (invoice_id) REFERENCES invoice(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_relay_tenant ON relay (tenant);
|
CREATE INDEX IF NOT EXISTS idx_activity_tenant_created ON activity (tenant_pubkey, created_at);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_activity_tenant_created ON activity (tenant, created_at);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_activity_resource_created ON activity (resource_id, created_at);
|
CREATE INDEX IF NOT EXISTS idx_activity_resource_created ON activity (resource_id, created_at);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_activity_unbilled ON activity (tenant, created_at) WHERE billed_at IS NULL;
|
CREATE INDEX IF NOT EXISTS idx_activity_unbilled ON activity (tenant_pubkey, created_at) WHERE billed_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_relay_tenant_pubkey ON relay (tenant_pubkey);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_invoice_tenant_created ON invoice (tenant_pubkey, created_at);
|
CREATE INDEX IF NOT EXISTS idx_invoice_tenant_created ON invoice (tenant_pubkey, created_at);
|
||||||
|
|
||||||
|
|||||||
+21
-23
@@ -79,7 +79,7 @@ impl Billing {
|
|||||||
command::set_tenant_billing_anchor(&tenant).await?;
|
command::set_tenant_billing_anchor(&tenant).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let invoice_item = self.reconcile_activity(&tenant, &activity).await?;
|
self.reconcile_activity(&tenant, &activity).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the tenant has no billing anchor, they have nothing to bill
|
// If the tenant has no billing anchor, they have nothing to bill
|
||||||
@@ -104,7 +104,7 @@ impl Billing {
|
|||||||
/// persist it with the activity's billed marker. Activities that produce no
|
/// persist it with the activity's billed marker. Activities that produce no
|
||||||
/// item (e.g. free-plan changes) are still marked billed so they aren't
|
/// item (e.g. free-plan changes) are still marked billed so they aren't
|
||||||
/// re-scanned.
|
/// re-scanned.
|
||||||
async fn reconcile_activity(&self, tenant: &Tenant, activity: &Activity) -> Result<Option<InvoiceItem>> {
|
async fn reconcile_activity(&self, tenant: &Tenant, activity: &Activity) -> Result<()> {
|
||||||
let invoice_item = match activity.activity_type.as_str() {
|
let invoice_item = match activity.activity_type.as_str() {
|
||||||
"create_relay" => {
|
"create_relay" => {
|
||||||
self.make_prorated_item(tenant, activity, 1, "New relay created")
|
self.make_prorated_item(tenant, activity, 1, "New relay created")
|
||||||
@@ -123,11 +123,9 @@ impl Billing {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match invoice_item {
|
match invoice_item {
|
||||||
Some(ref item) => command::insert_invoice_item_for_activity(&item, &activity.id).await?,
|
Some(ref item) => command::insert_invoice_item_for_activity(&item, &activity.id).await,
|
||||||
None => command::mark_activity_billed(&activity.id).await?,
|
None => command::mark_activity_billed(&activity.id).await,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(invoice_item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A prorated charge (or credit, with `sign` = -1) for the relay's current
|
/// A prorated charge (or credit, with `sign` = -1) for the relay's current
|
||||||
@@ -141,10 +139,10 @@ impl Billing {
|
|||||||
description: &str,
|
description: &str,
|
||||||
) -> Result<Option<InvoiceItem>> {
|
) -> Result<Option<InvoiceItem>> {
|
||||||
let Some(relay) = query::get_relay(&activity.resource_id).await? else {
|
let Some(relay) = query::get_relay(&activity.resource_id).await? else {
|
||||||
return anyhow!("activity resource was not a valid relay");
|
return Err(anyhow!("activity resource was not a valid relay"));
|
||||||
};
|
};
|
||||||
let Some(plan) = query::get_plan(&relay.plan) else {
|
let Some(plan) = query::get_plan(&relay.plan_id) else {
|
||||||
return anyhow!("activity plan was not a valid plan");
|
return Err(anyhow!("activity plan was not a valid plan"));
|
||||||
};
|
};
|
||||||
if plan.amount <= 0 {
|
if plan.amount <= 0 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -158,9 +156,9 @@ impl Billing {
|
|||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
invoice_id: None,
|
invoice_id: None,
|
||||||
activity_id: Some(activity.id.clone()),
|
activity_id: Some(activity.id.clone()),
|
||||||
tenant_pubkey: activity.tenant.clone(),
|
tenant_pubkey: activity.tenant_pubkey.clone(),
|
||||||
relay_id: activity.resource_id.clone(),
|
relay_id: activity.resource_id.clone(),
|
||||||
plan: plan.id,
|
plan_id: plan.id,
|
||||||
amount,
|
amount,
|
||||||
description: description.to_string(),
|
description: description.to_string(),
|
||||||
created_at: activity.created_at,
|
created_at: activity.created_at,
|
||||||
@@ -178,21 +176,21 @@ impl Billing {
|
|||||||
activity: &Activity,
|
activity: &Activity,
|
||||||
) -> Result<Option<InvoiceItem>> {
|
) -> Result<Option<InvoiceItem>> {
|
||||||
let Some(new_plan_id) = activity.plan_id.as_deref() else {
|
let Some(new_plan_id) = activity.plan_id.as_deref() else {
|
||||||
return anyhow!("activity plan was not a valid plan");
|
return Err(anyhow!("activity plan was not a valid plan"));
|
||||||
};
|
};
|
||||||
let Some(old_plan_id) =
|
let Some(old_plan_id) =
|
||||||
query::get_relay_plan_before(&activity.resource_id, activity.created_at).await?
|
query::get_relay_plan_before(&activity.resource_id, activity.created_at).await?
|
||||||
else {
|
else {
|
||||||
return anyhow!("no previous plan found for relay update activity");
|
return Err(anyhow!("no previous plan found for relay update activity"));
|
||||||
};
|
};
|
||||||
if old_plan_id == new_plan_id {
|
if old_plan_id == new_plan_id {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let Some(new_plan) = query::get_plan(new_plan_id) else {
|
let Some(new_plan) = query::get_plan(new_plan_id) else {
|
||||||
return anyhow!("new plan is an invalid plan");
|
return Err(anyhow!("new plan is an invalid plan"));
|
||||||
};
|
};
|
||||||
let Some(old_plan) = query::get_plan(old_plan_id) else {
|
let Some(old_plan) = query::get_plan(&old_plan_id) else {
|
||||||
return anyhow!("old plan is an invalid plan");
|
return Err(anyhow!("old plan is an invalid plan"));
|
||||||
};
|
};
|
||||||
|
|
||||||
let period = BillingPeriod::at(tenant, activity.created_at)
|
let period = BillingPeriod::at(tenant, activity.created_at)
|
||||||
@@ -208,9 +206,9 @@ impl Billing {
|
|||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
invoice_id: None,
|
invoice_id: None,
|
||||||
activity_id: Some(activity.id.clone()),
|
activity_id: Some(activity.id.clone()),
|
||||||
tenant_pubkey: activity.tenant.clone(),
|
tenant_pubkey: activity.tenant_pubkey.clone(),
|
||||||
relay_id: activity.resource_id.clone(),
|
relay_id: activity.resource_id.clone(),
|
||||||
plan: new_plan.id,
|
plan_id: new_plan.id,
|
||||||
amount,
|
amount,
|
||||||
description,
|
description,
|
||||||
created_at: activity.created_at,
|
created_at: activity.created_at,
|
||||||
@@ -232,7 +230,7 @@ impl Billing {
|
|||||||
if !state.active {
|
if !state.active {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(plan) = state.plan.and_then(|id| query::get_plan(&id)) else {
|
let Some(plan) = state.plan_id.and_then(|id| query::get_plan(&id)) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if plan.amount <= 0 {
|
if plan.amount <= 0 {
|
||||||
@@ -244,7 +242,7 @@ impl Billing {
|
|||||||
activity_id: None,
|
activity_id: None,
|
||||||
tenant_pubkey: tenant.pubkey.clone(),
|
tenant_pubkey: tenant.pubkey.clone(),
|
||||||
relay_id,
|
relay_id,
|
||||||
plan: plan.id,
|
plan_id: plan.id,
|
||||||
amount: plan.amount,
|
amount: plan.amount,
|
||||||
description: "Subscription renewal".to_string(),
|
description: "Subscription renewal".to_string(),
|
||||||
created_at: period.start,
|
created_at: period.start,
|
||||||
@@ -483,7 +481,7 @@ impl BillingPeriod {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct RelayState {
|
struct RelayState {
|
||||||
active: bool,
|
active: bool,
|
||||||
plan: Option<String>,
|
plan_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fold relay activities (which must be oldest-first) into each relay's
|
/// Fold relay activities (which must be oldest-first) into each relay's
|
||||||
@@ -498,11 +496,11 @@ fn relay_states(activities: &[Activity]) -> HashMap<String, RelayState> {
|
|||||||
match activity.activity_type.as_str() {
|
match activity.activity_type.as_str() {
|
||||||
"create_relay" => {
|
"create_relay" => {
|
||||||
state.active = true;
|
state.active = true;
|
||||||
state.plan = activity.plan_id.clone();
|
state.plan_id = activity.plan_id.clone();
|
||||||
}
|
}
|
||||||
"update_relay" => {
|
"update_relay" => {
|
||||||
if activity.plan_id.is_some() {
|
if activity.plan_id.is_some() {
|
||||||
state.plan = activity.plan_id.clone();
|
state.plan_id = activity.plan_id.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"activate_relay" => state.active = true,
|
"activate_relay" => state.active = true,
|
||||||
|
|||||||
+15
-15
@@ -66,7 +66,7 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
|
|||||||
let activity = with_tx(async |tx| {
|
let activity = with_tx(async |tx| {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO relay (
|
"INSERT INTO relay (
|
||||||
id, tenant, subdomain, plan, status, synced, sync_error,
|
id, tenant_pubkey, subdomain, plan_id, status, synced, sync_error,
|
||||||
info_name, info_icon, info_description,
|
info_name, info_icon, info_description,
|
||||||
policy_public_join, policy_strip_signatures,
|
policy_public_join, policy_strip_signatures,
|
||||||
groups_enabled, management_enabled, blossom_enabled,
|
groups_enabled, management_enabled, blossom_enabled,
|
||||||
@@ -74,9 +74,9 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
|
|||||||
) VALUES (?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
) VALUES (?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&relay.id)
|
.bind(&relay.id)
|
||||||
.bind(&relay.tenant)
|
.bind(&relay.tenant_pubkey)
|
||||||
.bind(&relay.subdomain)
|
.bind(&relay.subdomain)
|
||||||
.bind(&relay.plan)
|
.bind(&relay.plan_id)
|
||||||
.bind(&relay.sync_error)
|
.bind(&relay.sync_error)
|
||||||
.bind(&relay.info_name)
|
.bind(&relay.info_name)
|
||||||
.bind(&relay.info_icon)
|
.bind(&relay.info_icon)
|
||||||
@@ -90,7 +90,7 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
|
|||||||
.bind(relay.push_enabled)
|
.bind(relay.push_enabled)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await?;
|
.await?;
|
||||||
insert_activity_tx(tx, "create_relay", "relay", &relay.id, Some(&relay.plan)).await
|
insert_activity_tx(tx, "create_relay", "relay", &relay.id, Some(&relay.plan_id)).await
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
publish(activity);
|
publish(activity);
|
||||||
@@ -101,16 +101,16 @@ pub async fn update_relay(relay: &Relay) -> Result<()> {
|
|||||||
let activity = with_tx(async |tx| {
|
let activity = with_tx(async |tx| {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE relay
|
"UPDATE relay
|
||||||
SET tenant = ?, subdomain = ?, plan = ?, status = ?, sync_error = ?, synced = 0,
|
SET tenant_pubkey = ?, subdomain = ?, plan_id = ?, status = ?, sync_error = ?, synced = 0,
|
||||||
info_name = ?, info_icon = ?, info_description = ?,
|
info_name = ?, info_icon = ?, info_description = ?,
|
||||||
policy_public_join = ?, policy_strip_signatures = ?,
|
policy_public_join = ?, policy_strip_signatures = ?,
|
||||||
groups_enabled = ?, management_enabled = ?, blossom_enabled = ?,
|
groups_enabled = ?, management_enabled = ?, blossom_enabled = ?,
|
||||||
livekit_enabled = ?, push_enabled = ?
|
livekit_enabled = ?, push_enabled = ?
|
||||||
WHERE id = ?",
|
WHERE id = ?",
|
||||||
)
|
)
|
||||||
.bind(&relay.tenant)
|
.bind(&relay.tenant_pubkey)
|
||||||
.bind(&relay.subdomain)
|
.bind(&relay.subdomain)
|
||||||
.bind(&relay.plan)
|
.bind(&relay.plan_id)
|
||||||
.bind(&relay.status)
|
.bind(&relay.status)
|
||||||
.bind(&relay.sync_error)
|
.bind(&relay.sync_error)
|
||||||
.bind(&relay.info_name)
|
.bind(&relay.info_name)
|
||||||
@@ -126,7 +126,7 @@ pub async fn update_relay(relay: &Relay) -> Result<()> {
|
|||||||
.bind(&relay.id)
|
.bind(&relay.id)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await?;
|
.await?;
|
||||||
insert_activity_tx(tx, "update_relay", "relay", &relay.id, Some(&relay.plan)).await
|
insert_activity_tx(tx, "update_relay", "relay", &relay.id, Some(&relay.plan_id)).await
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
publish(activity);
|
publish(activity);
|
||||||
@@ -376,10 +376,10 @@ async fn insert_activity_tx(
|
|||||||
resource_id: &str,
|
resource_id: &str,
|
||||||
plan_id: Option<&str>,
|
plan_id: Option<&str>,
|
||||||
) -> Result<Activity> {
|
) -> Result<Activity> {
|
||||||
let tenant = match resource_type {
|
let tenant_pubkey = match resource_type {
|
||||||
"tenant" => resource_id.to_string(),
|
"tenant" => resource_id.to_string(),
|
||||||
"relay" => {
|
"relay" => {
|
||||||
sqlx::query_scalar::<_, String>("SELECT tenant FROM relay WHERE id = ?")
|
sqlx::query_scalar::<_, String>("SELECT tenant_pubkey FROM relay WHERE id = ?")
|
||||||
.bind(resource_id)
|
.bind(resource_id)
|
||||||
.fetch_one(&mut **tx)
|
.fetch_one(&mut **tx)
|
||||||
.await?
|
.await?
|
||||||
@@ -391,11 +391,11 @@ async fn insert_activity_tx(
|
|||||||
let created_at = chrono::Utc::now().timestamp();
|
let created_at = chrono::Utc::now().timestamp();
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO activity (id, tenant, created_at, activity_type, resource_type, resource_id, plan_id)
|
"INSERT INTO activity (id, tenant_pubkey, created_at, activity_type, resource_type, resource_id, plan_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&id)
|
.bind(&id)
|
||||||
.bind(&tenant)
|
.bind(&tenant_pubkey)
|
||||||
.bind(created_at)
|
.bind(created_at)
|
||||||
.bind(activity_type)
|
.bind(activity_type)
|
||||||
.bind(resource_type)
|
.bind(resource_type)
|
||||||
@@ -406,7 +406,7 @@ async fn insert_activity_tx(
|
|||||||
|
|
||||||
Ok(Activity {
|
Ok(Activity {
|
||||||
id,
|
id,
|
||||||
tenant,
|
tenant_pubkey,
|
||||||
created_at,
|
created_at,
|
||||||
activity_type: activity_type.to_string(),
|
activity_type: activity_type.to_string(),
|
||||||
resource_type: resource_type.to_string(),
|
resource_type: resource_type.to_string(),
|
||||||
@@ -441,7 +441,7 @@ async fn insert_invoice_tx(
|
|||||||
async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &InvoiceItem) -> Result<()> {
|
async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &InvoiceItem) -> Result<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO invoice_item
|
"INSERT INTO invoice_item
|
||||||
(id, invoice_id, activity_id, tenant_pubkey, relay_id, plan, amount, description, created_at)
|
(id, invoice_id, activity_id, tenant_pubkey, relay_id, plan_id, amount, description, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(&item.id)
|
.bind(&item.id)
|
||||||
@@ -449,7 +449,7 @@ async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &Invoice
|
|||||||
.bind(&item.activity_id)
|
.bind(&item.activity_id)
|
||||||
.bind(&item.tenant_pubkey)
|
.bind(&item.tenant_pubkey)
|
||||||
.bind(&item.relay_id)
|
.bind(&item.relay_id)
|
||||||
.bind(&item.plan)
|
.bind(&item.plan_id)
|
||||||
.bind(item.amount)
|
.bind(item.amount)
|
||||||
.bind(&item.description)
|
.bind(&item.description)
|
||||||
.bind(item.created_at)
|
.bind(item.created_at)
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ async fn try_sync_relay(relay: &Relay) -> Result<()> {
|
|||||||
"name": relay.info_name,
|
"name": relay.info_name,
|
||||||
"icon": relay.info_icon,
|
"icon": relay.info_icon,
|
||||||
"description": relay.info_description,
|
"description": relay.info_description,
|
||||||
"pubkey": relay.tenant,
|
"pubkey": relay.tenant_pubkey,
|
||||||
},
|
},
|
||||||
"policy": {
|
"policy": {
|
||||||
"public_join": relay.policy_public_join == 1,
|
"public_join": relay.policy_public_join == 1,
|
||||||
|
|||||||
+19
-19
@@ -4,20 +4,6 @@ pub const RELAY_STATUS_ACTIVE: &str = "active";
|
|||||||
pub const RELAY_STATUS_INACTIVE: &str = "inactive";
|
pub const RELAY_STATUS_INACTIVE: &str = "inactive";
|
||||||
pub const RELAY_STATUS_DELINQUENT: &str = "delinquent";
|
pub const RELAY_STATUS_DELINQUENT: &str = "delinquent";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
||||||
pub struct Activity {
|
|
||||||
pub id: String,
|
|
||||||
pub tenant: String,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub activity_type: String,
|
|
||||||
pub resource_type: String,
|
|
||||||
pub resource_id: String,
|
|
||||||
pub billed_at: Option<i64>,
|
|
||||||
/// The relay's plan at the time of a `create_relay`/`update_relay` activity;
|
|
||||||
/// `None` for all other activity types.
|
|
||||||
pub plan_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Plan {
|
pub struct Plan {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -41,12 +27,26 @@ pub struct Tenant {
|
|||||||
pub renewed_at: Option<i64>,
|
pub renewed_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
|
pub struct Activity {
|
||||||
|
pub id: String,
|
||||||
|
pub tenant_pubkey: String,
|
||||||
|
pub created_at: i64,
|
||||||
|
pub activity_type: String,
|
||||||
|
pub resource_type: String,
|
||||||
|
pub resource_id: String,
|
||||||
|
pub billed_at: Option<i64>,
|
||||||
|
/// The relay's plan at the time of a `create_relay`/`update_relay` activity;
|
||||||
|
/// `None` for all other activity types.
|
||||||
|
pub plan_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
pub struct Relay {
|
pub struct Relay {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub tenant: String,
|
pub tenant_pubkey: String,
|
||||||
pub subdomain: String,
|
pub subdomain: String,
|
||||||
pub plan: String,
|
pub plan_id: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub sync_error: String,
|
pub sync_error: String,
|
||||||
pub info_name: String,
|
pub info_name: String,
|
||||||
@@ -66,9 +66,9 @@ impl Default for Relay {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: String::new(),
|
id: String::new(),
|
||||||
tenant: String::new(),
|
tenant_pubkey: String::new(),
|
||||||
subdomain: String::new(),
|
subdomain: String::new(),
|
||||||
plan: String::new(),
|
plan_id: String::new(),
|
||||||
status: RELAY_STATUS_ACTIVE.to_string(),
|
status: RELAY_STATUS_ACTIVE.to_string(),
|
||||||
sync_error: String::new(),
|
sync_error: String::new(),
|
||||||
info_name: String::new(),
|
info_name: String::new(),
|
||||||
@@ -106,7 +106,7 @@ pub struct InvoiceItem {
|
|||||||
pub activity_id: Option<String>,
|
pub activity_id: Option<String>,
|
||||||
pub tenant_pubkey: String,
|
pub tenant_pubkey: String,
|
||||||
pub relay_id: String,
|
pub relay_id: String,
|
||||||
pub plan: String,
|
pub plan_id: String,
|
||||||
pub amount: i64,
|
pub amount: i64,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
|
|||||||
@@ -81,9 +81,9 @@ pub async fn list_relays_pending_sync() -> Result<Vec<Relay>> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_relays_for_tenant(tenant_id: &str) -> Result<Vec<Relay>> {
|
pub async fn list_relays_for_tenant(tenant_pubkey: &str) -> Result<Vec<Relay>> {
|
||||||
Ok(sqlx::query_as::<_, Relay>(&select_relay("WHERE tenant = ?"))
|
Ok(sqlx::query_as::<_, Relay>(&select_relay("WHERE tenant_pubkey = ?"))
|
||||||
.bind(tenant_id)
|
.bind(tenant_pubkey)
|
||||||
.fetch_all(pool())
|
.fetch_all(pool())
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ pub async fn get_bolt11_for_invoice(invoice_id: &str) -> Result<Option<Bolt11>>
|
|||||||
/// Ordered oldest-first so line items and proration apply in event order.
|
/// Ordered oldest-first so line items and proration apply in event order.
|
||||||
pub async fn list_billable_activity_for_tenant(tenant_pubkey: &str) -> Result<Vec<Activity>> {
|
pub async fn list_billable_activity_for_tenant(tenant_pubkey: &str) -> Result<Vec<Activity>> {
|
||||||
Ok(sqlx::query_as::<_, Activity>(&select_activity(
|
Ok(sqlx::query_as::<_, Activity>(&select_activity(
|
||||||
"WHERE tenant = ?
|
"WHERE tenant_pubkey = ?
|
||||||
AND billed_at IS NULL
|
AND billed_at IS NULL
|
||||||
AND activity_type IN (
|
AND activity_type IN (
|
||||||
'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay'
|
'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay'
|
||||||
@@ -197,7 +197,7 @@ pub async fn list_relay_activity_before(
|
|||||||
before: i64,
|
before: i64,
|
||||||
) -> Result<Vec<Activity>> {
|
) -> Result<Vec<Activity>> {
|
||||||
Ok(sqlx::query_as::<_, Activity>(&select_activity(
|
Ok(sqlx::query_as::<_, Activity>(&select_activity(
|
||||||
"WHERE tenant = ?
|
"WHERE tenant_pubkey = ?
|
||||||
AND resource_type = 'relay'
|
AND resource_type = 'relay'
|
||||||
AND activity_type IN (
|
AND activity_type IN (
|
||||||
'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay'
|
'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay'
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub async fn get_relay(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
let relay = api.get_relay_or_404(&id).await?;
|
let relay = api.get_relay_or_404(&id).await?;
|
||||||
api.require_admin_or_tenant(&auth, &relay.tenant)?;
|
api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
|
||||||
ok(relay)
|
ok(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ pub async fn list_relay_activity(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
let relay = api.get_relay_or_404(&id).await?;
|
let relay = api.get_relay_or_404(&id).await?;
|
||||||
api.require_admin_or_tenant(&auth, &relay.tenant)?;
|
api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
|
||||||
|
|
||||||
let activity = query::list_activity_for_resource(&id)
|
let activity = query::list_activity_for_resource(&id)
|
||||||
.await
|
.await
|
||||||
@@ -58,7 +58,7 @@ pub async fn list_relay_members(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
let relay = api.get_relay_or_404(&id).await?;
|
let relay = api.get_relay_or_404(&id).await?;
|
||||||
api.require_admin_or_tenant(&auth, &relay.tenant)?;
|
api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
|
||||||
|
|
||||||
let members = fetch_relay_members(&relay).await.map_err(internal)?;
|
let members = fetch_relay_members(&relay).await.map_err(internal)?;
|
||||||
ok(serde_json::json!({ "members": members }))
|
ok(serde_json::json!({ "members": members }))
|
||||||
@@ -66,9 +66,9 @@ pub async fn list_relay_members(
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateRelayRequest {
|
pub struct CreateRelayRequest {
|
||||||
pub tenant: String,
|
pub tenant_pubkey: String,
|
||||||
pub subdomain: String,
|
pub subdomain: String,
|
||||||
pub plan: String,
|
pub plan_id: String,
|
||||||
pub info_name: String,
|
pub info_name: String,
|
||||||
pub info_icon: String,
|
pub info_icon: String,
|
||||||
pub info_description: String,
|
pub info_description: String,
|
||||||
@@ -86,7 +86,7 @@ pub async fn create_relay(
|
|||||||
AuthedPubkey(auth): AuthedPubkey,
|
AuthedPubkey(auth): AuthedPubkey,
|
||||||
Json(payload): Json<CreateRelayRequest>,
|
Json(payload): Json<CreateRelayRequest>,
|
||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
api.require_admin_or_tenant(&auth, &payload.tenant)?;
|
api.require_admin_or_tenant(&auth, &payload.tenant_pubkey)?;
|
||||||
|
|
||||||
let relay_id = format!(
|
let relay_id = format!(
|
||||||
"{}_{}",
|
"{}_{}",
|
||||||
@@ -96,9 +96,9 @@ pub async fn create_relay(
|
|||||||
|
|
||||||
let relay = Relay {
|
let relay = Relay {
|
||||||
id: relay_id.clone(),
|
id: relay_id.clone(),
|
||||||
tenant: payload.tenant,
|
tenant_pubkey: payload.tenant_pubkey,
|
||||||
subdomain: payload.subdomain,
|
subdomain: payload.subdomain,
|
||||||
plan: payload.plan,
|
plan_id: payload.plan_id,
|
||||||
info_name: payload.info_name,
|
info_name: payload.info_name,
|
||||||
info_icon: payload.info_icon,
|
info_icon: payload.info_icon,
|
||||||
info_description: payload.info_description,
|
info_description: payload.info_description,
|
||||||
@@ -124,7 +124,7 @@ pub async fn create_relay(
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UpdateRelayRequest {
|
pub struct UpdateRelayRequest {
|
||||||
pub subdomain: Option<String>,
|
pub subdomain: Option<String>,
|
||||||
pub plan: Option<String>,
|
pub plan_id: Option<String>,
|
||||||
pub info_name: Option<String>,
|
pub info_name: Option<String>,
|
||||||
pub info_icon: Option<String>,
|
pub info_icon: Option<String>,
|
||||||
pub info_description: Option<String>,
|
pub info_description: Option<String>,
|
||||||
@@ -145,16 +145,16 @@ pub async fn update_relay(
|
|||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
let mut relay = api.get_relay_or_404(&id).await?;
|
let mut relay = api.get_relay_or_404(&id).await?;
|
||||||
|
|
||||||
api.require_admin_or_tenant(&auth, &relay.tenant)?;
|
api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
|
||||||
|
|
||||||
let current_plan = relay.plan.clone();
|
let current_plan = relay.plan_id.clone();
|
||||||
let requested_plan = payload.plan.clone();
|
let requested_plan = payload.plan_id.clone();
|
||||||
|
|
||||||
if let Some(v) = payload.subdomain {
|
if let Some(v) = payload.subdomain {
|
||||||
relay.subdomain = v;
|
relay.subdomain = v;
|
||||||
}
|
}
|
||||||
if let Some(v) = requested_plan.clone() {
|
if let Some(v) = requested_plan.clone() {
|
||||||
relay.plan = v;
|
relay.plan_id = v;
|
||||||
}
|
}
|
||||||
if let Some(v) = payload.info_name {
|
if let Some(v) = payload.info_name {
|
||||||
relay.info_name = v;
|
relay.info_name = v;
|
||||||
@@ -195,7 +195,7 @@ pub async fn update_relay(
|
|||||||
|
|
||||||
if plan_changed {
|
if plan_changed {
|
||||||
let selected_plan =
|
let selected_plan =
|
||||||
query::get_plan(&relay.plan).expect("validated plan must exist");
|
query::get_plan(&relay.plan_id).expect("validated plan must exist");
|
||||||
if let Some(limit) = selected_plan.members {
|
if let Some(limit) = selected_plan.members {
|
||||||
let current_members = fetch_relay_members(&relay)
|
let current_members = fetch_relay_members(&relay)
|
||||||
.await
|
.await
|
||||||
@@ -225,7 +225,7 @@ pub async fn deactivate_relay(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
let relay = api.get_relay_or_404(&id).await?;
|
let relay = api.get_relay_or_404(&id).await?;
|
||||||
api.require_admin_or_tenant(&auth, &relay.tenant)?;
|
api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
|
||||||
|
|
||||||
if relay.status == RELAY_STATUS_DELINQUENT {
|
if relay.status == RELAY_STATUS_DELINQUENT {
|
||||||
return Err(bad_request("relay-is-delinquent", "relay is delinquent"));
|
return Err(bad_request("relay-is-delinquent", "relay is delinquent"));
|
||||||
@@ -248,7 +248,7 @@ pub async fn reactivate_relay(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> ApiResult {
|
) -> ApiResult {
|
||||||
let relay = api.get_relay_or_404(&id).await?;
|
let relay = api.get_relay_or_404(&id).await?;
|
||||||
api.require_admin_or_tenant(&auth, &relay.tenant)?;
|
api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
|
||||||
|
|
||||||
if relay.status == RELAY_STATUS_DELINQUENT {
|
if relay.status == RELAY_STATUS_DELINQUENT {
|
||||||
return Err(bad_request("relay-is-delinquent", "relay is delinquent"));
|
return Err(bad_request("relay-is-delinquent", "relay is delinquent"));
|
||||||
@@ -287,7 +287,7 @@ fn prepare_relay(mut relay: Relay) -> Result<Relay, ApiError> {
|
|||||||
return Err(unprocessable("invalid-subdomain", "subdomain is invalid"));
|
return Err(unprocessable("invalid-subdomain", "subdomain is invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = query::get_plan(&relay.plan)
|
let plan = query::get_plan(&relay.plan_id)
|
||||||
.ok_or_else(|| unprocessable("invalid-plan", "plan not found"))?;
|
.ok_or_else(|| unprocessable("invalid-plan", "plan not found"))?;
|
||||||
|
|
||||||
if (!plan.blossom && relay.blossom_enabled == 1) || (!plan.livekit && relay.livekit_enabled == 1) {
|
if (!plan.blossom && relay.blossom_enabled == 1) || (!plan.livekit && relay.livekit_enabled == 1) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
const billedRelays = createMemo(() => {
|
const billedRelays = createMemo(() => {
|
||||||
const planById = new Map(plans().map((p) => [p.id, p]))
|
const planById = new Map(plans().map((p) => [p.id, p]))
|
||||||
return (relays() ?? [])
|
return (relays() ?? [])
|
||||||
.map((relay) => ({ relay, plan: planById.get(relay.plan) }))
|
.map((relay) => ({ relay, plan: planById.get(relay.plan_id) }))
|
||||||
.filter((entry) => Boolean(entry.plan?.amount))
|
.filter((entry) => Boolean(entry.plan?.amount))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
<li class="flex items-center justify-between gap-3 text-sm">
|
<li class="flex items-center justify-between gap-3 text-sm">
|
||||||
<span class="truncate text-gray-900">{relay.info_name || relay.subdomain}</span>
|
<span class="truncate text-gray-900">{relay.info_name || relay.subdomain}</span>
|
||||||
<span class="flex-shrink-0 text-xs text-gray-500">
|
<span class="flex-shrink-0 text-xs text-gray-500">
|
||||||
{plan?.name ?? relay.plan}
|
{plan?.name ?? relay.plan_id}
|
||||||
<Show when={plan}> · ${(plan!.amount / 100).toFixed(2)}/mo</Show>
|
<Show when={plan}> · ${(plan!.amount / 100).toFixed(2)}/mo</Show>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ function memberLabel(members: number | null) {
|
|||||||
|
|
||||||
type PricingTableProps = {
|
type PricingTableProps = {
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
selectedPlan?: PlanId
|
selectedPlanId?: PlanId
|
||||||
onSelect?: (plan: PlanId) => void
|
onSelect?: (planId: PlanId) => void
|
||||||
onCta?: (plan: PlanId) => void
|
onCta?: (planId: PlanId) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PricingTable(props: PricingTableProps) {
|
export default function PricingTable(props: PricingTableProps) {
|
||||||
@@ -43,7 +43,7 @@ export default function PricingTable(props: PricingTableProps) {
|
|||||||
<For each={plans()}>
|
<For each={plans()}>
|
||||||
{(plan) => {
|
{(plan) => {
|
||||||
const isPopular = plan.id === "basic"
|
const isPopular = plan.id === "basic"
|
||||||
const isSelected = () => props.selectable && props.selectedPlan === plan.id
|
const isSelected = () => props.selectable && props.selectedPlanId === plan.id
|
||||||
|
|
||||||
const card = (
|
const card = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ type RelayDetailCardProps = {
|
|||||||
onToggleMediaStorage?: () => void
|
onToggleMediaStorage?: () => void
|
||||||
onToggleLivekitSupport?: () => void
|
onToggleLivekitSupport?: () => void
|
||||||
onTogglePushNotifications?: () => void
|
onTogglePushNotifications?: () => void
|
||||||
onUpdatePlan?: (plan: PlanId) => Promise<void>
|
onUpdatePlan?: (planId: PlanId) => Promise<void>
|
||||||
enforcePlanLimits?: boolean
|
enforcePlanLimits?: boolean
|
||||||
showPlanActions?: boolean
|
showPlanActions?: boolean
|
||||||
}
|
}
|
||||||
@@ -76,17 +76,17 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
const [menuOpen, setMenuOpen] = createSignal(false)
|
const [menuOpen, setMenuOpen] = createSignal(false)
|
||||||
const [plan, setPlan] = createSignal<PlanId>(props.relay.plan)
|
const [planId, setPlanId] = createSignal<PlanId>(props.relay.plan_id)
|
||||||
const [pendingAction, setPendingAction] = createSignal<"deactivate" | "reactivate" | null>(null)
|
const [pendingAction, setPendingAction] = createSignal<"deactivate" | "reactivate" | null>(null)
|
||||||
|
|
||||||
let menuContainerRef: HTMLDivElement | undefined
|
let menuContainerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
const memberLimitLabel = () => {
|
const memberLimitLabel = () => {
|
||||||
const p = plans().find(p => p.id === r().plan)
|
const p = plans().find(p => p.id === r().plan_id)
|
||||||
if (!p) return "?"
|
if (!p) return "?"
|
||||||
return p.members === null ? "∞" : String(p.members)
|
return p.members === null ? "∞" : String(p.members)
|
||||||
}
|
}
|
||||||
const planLimited = () => (props.enforcePlanLimits ?? true) && r().plan === "free"
|
const planLimited = () => (props.enforcePlanLimits ?? true) && r().plan_id === "free"
|
||||||
const showPlanActions = () => props.showPlanActions ?? true
|
const showPlanActions = () => props.showPlanActions ?? true
|
||||||
const actionBusy = () => pendingAction() === "deactivate" ? !!props.deactivating : pendingAction() === "reactivate" ? !!props.reactivating : false
|
const actionBusy = () => pendingAction() === "deactivate" ? !!props.deactivating : pendingAction() === "reactivate" ? !!props.reactivating : false
|
||||||
const relayLabel = () => r().info_name || r().subdomain
|
const relayLabel = () => r().info_name || r().subdomain
|
||||||
@@ -107,11 +107,11 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
|
|||||||
const confirmBusyLabel = () => pendingAction() === "deactivate" ? "Deactivating..." : "Reactivating..."
|
const confirmBusyLabel = () => pendingAction() === "deactivate" ? "Deactivating..." : "Reactivating..."
|
||||||
const confirmTone = () => pendingAction() === "deactivate" ? "danger" : "primary"
|
const confirmTone = () => pendingAction() === "deactivate" ? "danger" : "primary"
|
||||||
|
|
||||||
async function changePlan(plan: PlanId) {
|
async function changePlanId(planId: PlanId) {
|
||||||
setPlan(plan)
|
setPlanId(planId)
|
||||||
try {
|
try {
|
||||||
await props.onUpdatePlan?.(plan)
|
await props.onUpdatePlan?.(planId)
|
||||||
setToastMessage(`Plan updated to ${plan}`, "success")
|
setToastMessage(`Plan updated to ${planId}`, "success")
|
||||||
} catch {
|
} catch {
|
||||||
// error is handled by the caller
|
// error is handled by the caller
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
|
|||||||
</Field>
|
</Field>
|
||||||
<Show when={props.showTenant}>
|
<Show when={props.showTenant}>
|
||||||
<Field label="Tenant">
|
<Field label="Tenant">
|
||||||
<span class="font-mono text-xs break-all">{r().tenant}</span>
|
<span class="font-mono text-xs break-all">{r().tenant_pubkey}</span>
|
||||||
</Field>
|
</Field>
|
||||||
</Show>
|
</Show>
|
||||||
</MembershipSection>
|
</MembershipSection>
|
||||||
@@ -373,15 +373,15 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
|
|||||||
when={props.onUpdatePlan}
|
when={props.onUpdatePlan}
|
||||||
fallback={
|
fallback={
|
||||||
<Field label="Current plan">
|
<Field label="Current plan">
|
||||||
<span class="capitalize text-gray-900">{r().plan}</span>
|
<span class="capitalize text-gray-900">{r().plan_id}</span>
|
||||||
</Field>
|
</Field>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="lg:col-span-2 space-y-4">
|
<div class="lg:col-span-2 space-y-4">
|
||||||
<PricingTable
|
<PricingTable
|
||||||
selectable
|
selectable
|
||||||
selectedPlan={plan()}
|
selectedPlanId={planId()}
|
||||||
onSelect={changePlan}
|
onSelect={changePlanId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { validateSubdomainLabel } from "@/lib/subdomain"
|
|||||||
import { setToastMessage } from "@/components/Toast"
|
import { setToastMessage } from "@/components/Toast"
|
||||||
import { plans } from "@/lib/state"
|
import { plans } from "@/lib/state"
|
||||||
|
|
||||||
export type RelayFormValues = Pick<Relay, "info_name" | "subdomain" | "info_icon" | "info_description" | "plan">
|
export type RelayFormValues = Pick<Relay, "info_name" | "subdomain" | "info_icon" | "info_description" | "plan_id">
|
||||||
|
|
||||||
type RelayFormProps = {
|
type RelayFormProps = {
|
||||||
initialValues?: Partial<RelayFormValues>
|
initialValues?: Partial<RelayFormValues>
|
||||||
@@ -16,8 +16,8 @@ type RelayFormProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RelayForm(props: RelayFormProps) {
|
export default function RelayForm(props: RelayFormProps) {
|
||||||
const defaultPlanId = createMemo(() => props.initialValues?.plan ?? plans()[0]?.id ?? "free")
|
const defaultPlanId = createMemo(() => props.initialValues?.plan_id ?? plans()[0]?.id ?? "free")
|
||||||
const [plan, setPlan] = createSignal(defaultPlanId())
|
const [planId, setPlanId] = createSignal(defaultPlanId())
|
||||||
const [name, setName] = createSignal(props.initialValues?.info_name ?? "")
|
const [name, setName] = createSignal(props.initialValues?.info_name ?? "")
|
||||||
const [subdomain, setSubdomain] = createSignal(props.initialValues?.subdomain ?? "")
|
const [subdomain, setSubdomain] = createSignal(props.initialValues?.subdomain ?? "")
|
||||||
const [icon, setIcon] = createSignal(props.initialValues?.info_icon ?? "")
|
const [icon, setIcon] = createSignal(props.initialValues?.info_icon ?? "")
|
||||||
@@ -27,7 +27,7 @@ export default function RelayForm(props: RelayFormProps) {
|
|||||||
async function handleSubmit(e: Event) {
|
async function handleSubmit(e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (!plan()) {
|
if (!planId()) {
|
||||||
setToastMessage("Please select a plan")
|
setToastMessage("Please select a plan")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ export default function RelayForm(props: RelayFormProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await props.onSubmit({
|
await props.onSubmit({
|
||||||
plan: plan(),
|
plan_id: planId(),
|
||||||
info_name: name(),
|
info_name: name(),
|
||||||
subdomain: subdomain(),
|
subdomain: subdomain(),
|
||||||
info_icon: icon(),
|
info_icon: icon(),
|
||||||
@@ -56,7 +56,7 @@ export default function RelayForm(props: RelayFormProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => setPlan(defaultPlanId()))
|
createEffect(() => setPlanId(defaultPlanId()))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.syncSubdomainWithName) {
|
if (props.syncSubdomainWithName) {
|
||||||
@@ -112,8 +112,8 @@ export default function RelayForm(props: RelayFormProps) {
|
|||||||
{(p) => (
|
{(p) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPlan(p.id)}
|
onClick={() => setPlanId(p.id)}
|
||||||
class={`border-2 rounded-xl p-4 text-left transition-colors ${plan() === p.id ? "border-blue-600 bg-blue-50" : "border-gray-200 hover:border-gray-300"}`}
|
class={`border-2 rounded-xl p-4 text-left transition-colors ${planId() === p.id ? "border-blue-600 bg-blue-50" : "border-gray-200 hover:border-gray-300"}`}
|
||||||
>
|
>
|
||||||
<div class="font-bold text-gray-900">{p.name}</div>
|
<div class="font-bold text-gray-900">{p.name}</div>
|
||||||
<div class="text-sm text-gray-500 mt-1">
|
<div class="text-sm text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function RelayListItem(props: RelayListItemProps) {
|
|||||||
<p class="font-medium text-gray-900">{props.relay.info_name || props.relay.subdomain}</p>
|
<p class="font-medium text-gray-900">{props.relay.info_name || props.relay.subdomain}</p>
|
||||||
<p class="text-xs text-gray-500">{props.relay.subdomain}.spaces.coracle.social</p>
|
<p class="text-xs text-gray-500">{props.relay.subdomain}.spaces.coracle.social</p>
|
||||||
{props.showTenant && (
|
{props.showTenant && (
|
||||||
<p class="text-xs text-gray-500 break-all mt-1">Tenant: {props.relay.tenant}</p>
|
<p class="text-xs text-gray-500 break-all mt-1">Tenant: {props.relay.tenant_pubkey}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export type PlanId = string
|
|||||||
|
|
||||||
export type Relay = {
|
export type Relay = {
|
||||||
id: string
|
id: string
|
||||||
tenant: string
|
tenant_pubkey: string
|
||||||
subdomain: string
|
subdomain: string
|
||||||
plan: PlanId
|
plan_id: PlanId
|
||||||
status: string
|
status: string
|
||||||
sync_error: string
|
sync_error: string
|
||||||
synced: number
|
synced: number
|
||||||
@@ -63,9 +63,9 @@ export type Relay = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CreateRelayInput = {
|
export type CreateRelayInput = {
|
||||||
tenant?: string
|
tenant_pubkey?: string
|
||||||
subdomain: string
|
subdomain: string
|
||||||
plan: string
|
plan_id: string
|
||||||
info_name?: string
|
info_name?: string
|
||||||
info_icon?: string
|
info_icon?: string
|
||||||
info_description?: string
|
info_description?: string
|
||||||
@@ -80,7 +80,7 @@ export type CreateRelayInput = {
|
|||||||
|
|
||||||
export type UpdateRelayInput = {
|
export type UpdateRelayInput = {
|
||||||
subdomain?: string
|
subdomain?: string
|
||||||
plan?: string
|
plan_id?: string
|
||||||
info_name?: string
|
info_name?: string
|
||||||
info_icon?: string
|
info_icon?: string
|
||||||
info_description?: string
|
info_description?: string
|
||||||
@@ -115,7 +115,7 @@ export type Invoice = {
|
|||||||
|
|
||||||
export type Activity = {
|
export type Activity = {
|
||||||
id: string
|
id: string
|
||||||
tenant: string
|
tenant_pubkey: string
|
||||||
created_at: number
|
created_at: number
|
||||||
activity_type: string
|
activity_type: string
|
||||||
resource_type: string
|
resource_type: string
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ export const createRelayForActiveTenant = (input: CreateRelayInput) => {
|
|||||||
|
|
||||||
const overrides = {
|
const overrides = {
|
||||||
tenant: account()!.pubkey,
|
tenant: account()!.pubkey,
|
||||||
blossom_enabled: input.plan === "free" ? 0 : 1,
|
blossom_enabled: input.plan_id === "free" ? 0 : 1,
|
||||||
livekit_enabled: input.plan === "free" ? 0 : 1,
|
livekit_enabled: input.plan_id === "free" ? 0 : 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
return createRelay({...defaults, ...input, ...overrides})
|
return createRelay({...defaults, ...input, ...overrides})
|
||||||
@@ -127,7 +127,7 @@ export const updateActiveTenant = (input: { nwc_url?: string }) => updateTenant(
|
|||||||
|
|
||||||
export const updateRelayById = (id: string, input: UpdateRelayInput) => updateRelay(id, input)
|
export const updateRelayById = (id: string, input: UpdateRelayInput) => updateRelay(id, input)
|
||||||
|
|
||||||
export const updateRelayPlanById = (id: string, plan: string) => updateRelay(id, { plan })
|
export const updateRelayPlanById = (id: string, plan_id: string) => updateRelay(id, { plan_id })
|
||||||
|
|
||||||
export const deactivateRelayById = (id: string) => deactivateRelay(id)
|
export const deactivateRelayById = (id: string) => deactivateRelay(id)
|
||||||
|
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ export default function useRelayToggles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpdatePlan(plan: PlanId) {
|
async function handleUpdatePlan(plan_id: PlanId) {
|
||||||
const current = relay()
|
const current = relay()
|
||||||
if (!current) return
|
if (!current) return
|
||||||
|
|
||||||
const previous = current
|
const previous = current
|
||||||
const next = { ...current, plan }
|
const next = { ...current, plan_id }
|
||||||
const update: Record<string, unknown> = { plan }
|
const update: Record<string, unknown> = { plan_id }
|
||||||
if (plan === "free") {
|
if (plan_id === "free") {
|
||||||
next.blossom_enabled = 0
|
next.blossom_enabled = 0
|
||||||
next.livekit_enabled = 0
|
next.livekit_enabled = 0
|
||||||
update.blossom_enabled = 0
|
update.blossom_enabled = 0
|
||||||
@@ -101,7 +101,7 @@ export default function useRelayToggles(
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plan !== "free") {
|
if (plan_id !== "free") {
|
||||||
const needsSetup = await tenantNeedsPaymentSetup()
|
const needsSetup = await tenantNeedsPaymentSetup()
|
||||||
if (needsSetup) {
|
if (needsSetup) {
|
||||||
const invoice = await getLatestOpenInvoice()
|
const invoice = await getLatestOpenInvoice()
|
||||||
@@ -116,9 +116,9 @@ export default function useRelayToggles(
|
|||||||
onToggleStripSignatures: () => toggle("policy_strip_signatures", false),
|
onToggleStripSignatures: () => toggle("policy_strip_signatures", false),
|
||||||
onToggleGroups: () => toggle("groups_enabled", true),
|
onToggleGroups: () => toggle("groups_enabled", true),
|
||||||
onToggleManagement: () => toggle("management_enabled", true),
|
onToggleManagement: () => toggle("management_enabled", true),
|
||||||
onToggleMediaStorage: () => toggle("blossom_enabled", relay()?.plan !== "free"),
|
onToggleMediaStorage: () => toggle("blossom_enabled", relay()?.plan_id !== "free"),
|
||||||
onTogglePushNotifications: () => toggle("push_enabled", true),
|
onTogglePushNotifications: () => toggle("push_enabled", true),
|
||||||
onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan !== "free"),
|
onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan_id !== "free"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice: () => setPendingInvoice(undefined), pendingPaymentSetup, clearPendingPaymentSetup: () => setPendingPaymentSetup(false), toggles }
|
return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice: () => setPendingInvoice(undefined), pendingPaymentSetup, clearPendingPaymentSetup: () => setPendingPaymentSetup(false), toggles }
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ export default function Home() {
|
|||||||
const [showRelayModal, setShowRelayModal] = createSignal(false)
|
const [showRelayModal, setShowRelayModal] = createSignal(false)
|
||||||
const [showLoginModal, setShowLoginModal] = createSignal(false)
|
const [showLoginModal, setShowLoginModal] = createSignal(false)
|
||||||
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
|
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
|
||||||
const [initialPlan, setInitialPlan] = createSignal<RelayFormValues["plan"]>("free")
|
const [initialPlanId, setInitialPlanId] = createSignal<RelayFormValues["plan_id"]>("free")
|
||||||
|
|
||||||
function openRelayModal(plan: RelayFormValues["plan"] = "free") {
|
function openRelayModal(planId: RelayFormValues["plan_id"] = "free") {
|
||||||
setInitialPlan(plan)
|
setInitialPlanId(planId)
|
||||||
setShowRelayModal(true)
|
setShowRelayModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ export default function Home() {
|
|||||||
|
|
||||||
<RelayForm
|
<RelayForm
|
||||||
syncSubdomainWithName
|
syncSubdomainWithName
|
||||||
initialValues={{ plan: initialPlan() }}
|
initialValues={{ plan_id: initialPlanId() }}
|
||||||
onSubmit={onRelayFormSubmit}
|
onSubmit={onRelayFormSubmit}
|
||||||
submitLabel="Continue"
|
submitLabel="Continue"
|
||||||
submittingLabel="Creating..."
|
submittingLabel="Creating..."
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function RelayDetail() {
|
|||||||
const isPaidRelay = createMemo(() => {
|
const isPaidRelay = createMemo(() => {
|
||||||
const r = relay()
|
const r = relay()
|
||||||
if (!r) return false
|
if (!r) return false
|
||||||
const plan = plans().find(p => p.id === r.plan)
|
const plan = plans().find(p => p.id === r.plan_id)
|
||||||
return !!(plan && plan.amount > 0)
|
return !!(plan && plan.amount > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function RelayNew() {
|
|||||||
const relay = await createRelayForActiveTenant(values)
|
const relay = await createRelayForActiveTenant(values)
|
||||||
createdRelayId = relay.id
|
createdRelayId = relay.id
|
||||||
|
|
||||||
if (values.plan !== "free") {
|
if (values.plan_id !== "free") {
|
||||||
const needsSetup = await tenantNeedsPaymentSetup()
|
const needsSetup = await tenantNeedsPaymentSetup()
|
||||||
if (needsSetup) {
|
if (needsSetup) {
|
||||||
const invoice = await getLatestOpenInvoice()
|
const invoice = await getLatestOpenInvoice()
|
||||||
|
|||||||
Reference in New Issue
Block a user