From 7e577bf7ff7a7797bbb14a74f9b794e40eeb7715 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Tue, 3 Mar 2026 10:16:09 -0800 Subject: [PATCH] Stabilize id/schema --- backend/migrations/0001_init.sql | 8 +- backend/migrations/0002_add_relay_icon.sql | 1 - .../migrations/0003_add_tenant_nwc_url.sql | 1 - .../0004_unique_relay_subdomain.sql | 2 - backend/migrations/0005_add_relay_config.sql | 1 - backend/src/api.rs | 19 +- backend/src/models.rs | 1 - backend/src/provisioning.rs | 206 +++++++++--------- backend/src/repo.rs | 13 +- justfile | 2 +- 10 files changed, 119 insertions(+), 135 deletions(-) delete mode 100644 backend/migrations/0002_add_relay_icon.sql delete mode 100644 backend/migrations/0003_add_tenant_nwc_url.sql delete mode 100644 backend/migrations/0004_unique_relay_subdomain.sql delete mode 100644 backend/migrations/0005_add_relay_config.sql diff --git a/backend/migrations/0001_init.sql b/backend/migrations/0001_init.sql index a287e33..809f2d8 100644 --- a/backend/migrations/0001_init.sql +++ b/backend/migrations/0001_init.sql @@ -1,17 +1,19 @@ CREATE TABLE IF NOT EXISTS tenants ( pubkey TEXT PRIMARY KEY, - status TEXT NOT NULL + status TEXT NOT NULL, + tenant_nwc_url TEXT NOT NULL DEFAULT "" ); CREATE TABLE IF NOT EXISTS relays ( id TEXT PRIMARY KEY, tenant TEXT NOT NULL, name TEXT NOT NULL, - subdomain TEXT NOT NULL, - schema TEXT NOT NULL, + subdomain TEXT NOT NULL UNIQUE, description TEXT NOT NULL, plan TEXT NOT NULL, status TEXT NOT NULL, + icon TEXT NOT NULL DEFAULT "", + config TEXT, FOREIGN KEY (tenant) REFERENCES tenants(pubkey) ); diff --git a/backend/migrations/0002_add_relay_icon.sql b/backend/migrations/0002_add_relay_icon.sql deleted file mode 100644 index 33a0fbc..0000000 --- a/backend/migrations/0002_add_relay_icon.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE relays ADD COLUMN icon TEXT NOT NULL DEFAULT ""; diff --git a/backend/migrations/0003_add_tenant_nwc_url.sql b/backend/migrations/0003_add_tenant_nwc_url.sql deleted file mode 100644 index 9c8f763..0000000 --- a/backend/migrations/0003_add_tenant_nwc_url.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE tenants ADD COLUMN tenant_nwc_url TEXT NOT NULL DEFAULT ""; diff --git a/backend/migrations/0004_unique_relay_subdomain.sql b/backend/migrations/0004_unique_relay_subdomain.sql deleted file mode 100644 index d7a3f99..0000000 --- a/backend/migrations/0004_unique_relay_subdomain.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE UNIQUE INDEX IF NOT EXISTS relays_subdomain_unique -ON relays (subdomain); diff --git a/backend/migrations/0005_add_relay_config.sql b/backend/migrations/0005_add_relay_config.sql deleted file mode 100644 index a41ad75..0000000 --- a/backend/migrations/0005_add_relay_config.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE relays ADD COLUMN config TEXT; diff --git a/backend/src/api.rs b/backend/src/api.rs index d77fe81..f8204b5 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -9,7 +9,6 @@ use axum::{ routing::{get, post, put}, }; use serde::{Deserialize, Serialize}; -use uuid::Uuid; use crate::auth::verify_nip98; use crate::models::{NewTenant, Relay, RelayConfig}; @@ -242,12 +241,12 @@ async fn create_tenant_relay( .into_response(); } + let id = payload.subdomain.replace('-', "_"); let relay = Relay { - id: Uuid::new_v4().to_string(), + id: id.clone(), tenant: pubkey.clone(), name: payload.name, subdomain: payload.subdomain.clone(), - schema: payload.subdomain.replace('-', "_"), icon: payload.icon, description: payload.description, plan: payload.plan, @@ -275,7 +274,7 @@ async fn create_tenant_relay( .into_response(); } - if let Err(err) = state.provisioner.sync_relay(&relay, true).await { + if let Err(err) = state.provisioner.create_relay(&relay).await { tracing::error!(relay_id = relay.id, error = %err, "zooid create failed"); let _ = state.repo.update_relay_status(&relay.id, "provisioning_failed").await; return ( @@ -362,7 +361,6 @@ async fn update_tenant_relay( tenant: existing.tenant, name: payload.name, subdomain: payload.subdomain.clone(), - schema: payload.subdomain.replace('-', "_"), icon: payload.icon, description: payload.description, plan: payload.plan, @@ -390,15 +388,17 @@ async fn update_tenant_relay( .into_response(); } - if let Err(err) = state.provisioner.sync_relay(&relay, false).await { + if let Err(err) = state.provisioner.update_relay(&relay).await { tracing::error!(relay_id = relay.id, error = %err, "zooid patch failed"); return ( StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError { error: format!("failed to update relay config: {err}") }), + Json(ApiError { error: format!("failed to provision relay: {err}") }), ) .into_response(); } + let _ = state.repo.update_relay_status(&relay.id, "active").await; + (StatusCode::OK, Json(relay)).into_response() } @@ -758,7 +758,6 @@ async fn admin_update_relay( tenant: existing.tenant, name: payload.name, subdomain: payload.subdomain.clone(), - schema: payload.subdomain.replace('-', "_"), icon: payload.icon, description: payload.description, plan: payload.plan, @@ -786,7 +785,7 @@ async fn admin_update_relay( .into_response(); } - if let Err(err) = state.provisioner.sync_relay(&relay, false).await { + if let Err(err) = state.provisioner.update_relay(&relay).await { tracing::error!(relay_id = relay.id, error = %err, "zooid patch failed"); return ( StatusCode::INTERNAL_SERVER_ERROR, @@ -846,5 +845,3 @@ async fn admin_deactivate_relay( (StatusCode::OK, Json(relay)).into_response() } - - diff --git a/backend/src/models.rs b/backend/src/models.rs index da35d42..10cdf8b 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -29,7 +29,6 @@ pub struct Relay { pub tenant: String, pub name: String, pub subdomain: String, - pub schema: String, pub icon: String, pub description: String, pub plan: String, diff --git a/backend/src/provisioning.rs b/backend/src/provisioning.rs index 56cb741..c4c7249 100644 --- a/backend/src/provisioning.rs +++ b/backend/src/provisioning.rs @@ -2,7 +2,6 @@ use anyhow::{Result, anyhow}; use rand::RngCore; use rand::rngs::OsRng; use reqwest::Client; -use serde::Serialize; use serde_json::{Value, json}; use nostr_sdk::nostr::Keys; @@ -36,49 +35,113 @@ impl Provisioner { }) } - /// Create or update a relay in zooid. + /// Create a relay in zooid. /// - /// On creation, POSTs the full config (including a generated secret and host). - /// On update, PATCHes only the mutable fields (info + config sections). - pub async fn sync_relay(&self, relay: &Relay, is_new: bool) -> Result<()> { + /// POSTs the full config (including a generated secret and host). + pub async fn create_relay(&self, relay: &Relay) -> Result<()> { let url = format!("{}/relay/{}", self.base_url.trim_end_matches('/'), relay.id); - if is_new { - let host = format!("{}.{}", relay.subdomain, self.relay_domain); - let secret = generate_secret_hex(); - let payload = build_full_config(relay, host, secret); - let auth = self.build_auth_header(&url, HttpMethod::POST).await?; + let blossom_default = relay.plan != "free"; + let cfg = relay.config.as_ref(); + let host = format!("{}.{}", relay.subdomain, self.relay_domain); + let secret = generate_secret_hex(); + let payload = json!({ + "host": host, + "schema": relay.id, + "secret": secret, + "info": { + "name": relay.name, + "icon": relay.icon, + "pubkey": relay.tenant, + "description": relay.description, + }, + "policy": { + "public_join": cfg_bool(cfg, |c| &c.policy, "public_join", false), + "strip_signatures": cfg_bool(cfg, |c| &c.policy, "strip_signatures", false), + }, + "groups": { + "enabled": cfg_bool(cfg, |c| &c.groups, "enabled", true), + "auto_join": cfg_bool(cfg, |c| &c.groups, "auto_join", true), + }, + "push": { + "enabled": cfg_bool(cfg, |c| &c.push, "enabled", true), + }, + "management": { + "enabled": cfg_bool(cfg, |c| &c.management, "enabled", true), + }, + "blossom": { + "enabled": cfg_bool(cfg, |c| &c.blossom, "enabled", blossom_default), + }, + "roles": { + "member": { "pubkeys": [], "can_invite": true, "can_manage": false } + }, + }); + let auth = self.build_auth_header(&url, HttpMethod::POST).await?; - let res = self - .client - .post(&url) - .header(reqwest::header::AUTHORIZATION, auth) - .json(&payload) - .send() - .await?; + let res = self + .client + .post(&url) + .header(reqwest::header::AUTHORIZATION, auth) + .json(&payload) + .send() + .await?; - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - return Err(anyhow!("zooid create failed: {} {}", status, body)); - } - } else { - let patch = build_patch(relay); - let auth = self.build_auth_header(&url, HttpMethod::PATCH).await?; + if !res.status().is_success() { + let status = res.status(); + let body = res.text().await.unwrap_or_default(); + return Err(anyhow!("zooid create failed: {} {}", status, body)); + } - let res = self - .client - .patch(&url) - .header(reqwest::header::AUTHORIZATION, auth) - .json(&patch) - .send() - .await?; + Ok(()) + } - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - return Err(anyhow!("zooid patch failed: {} {}", status, body)); - } + /// Update a relay in zooid. + /// + /// PATCHes only the mutable fields (info + config sections). + pub async fn update_relay(&self, relay: &Relay) -> Result<()> { + let url = format!("{}/relay/{}", self.base_url.trim_end_matches('/'), relay.id); + let host = format!("{}.{}", relay.subdomain, self.relay_domain); + let blossom_default = relay.plan != "free"; + let cfg = relay.config.as_ref(); + let patch = json!({ + "host": host, + "info": { + "name": relay.name, + "icon": relay.icon, + "description": relay.description, + }, + "policy": { + "public_join": cfg_bool(cfg, |c| &c.policy, "public_join", false), + "strip_signatures": cfg_bool(cfg, |c| &c.policy, "strip_signatures", false), + }, + "groups": { + "enabled": cfg_bool(cfg, |c| &c.groups, "enabled", true), + "auto_join": cfg_bool(cfg, |c| &c.groups, "auto_join", true), + }, + "push": { + "enabled": cfg_bool(cfg, |c| &c.push, "enabled", true), + }, + "management": { + "enabled": cfg_bool(cfg, |c| &c.management, "enabled", true), + }, + "blossom": { + "enabled": cfg_bool(cfg, |c| &c.blossom, "enabled", blossom_default), + }, + }); + let auth = self.build_auth_header(&url, HttpMethod::PATCH).await?; + + let res = self + .client + .patch(&url) + .header(reqwest::header::AUTHORIZATION, auth) + .json(&patch) + .send() + .await?; + + if !res.status().is_success() { + let status = res.status(); + let body = res.text().await.unwrap_or_default(); + return Err(anyhow!("zooid patch failed: {} {}", status, body)); } Ok(()) @@ -92,75 +155,6 @@ impl Provisioner { } } -/// Builds the full zooid config payload for relay creation (POST). -fn build_full_config(relay: &Relay, host: String, secret: String) -> Value { - let blossom_default = relay.plan != "free"; - let cfg = relay.config.as_ref(); - - json!({ - "host": host, - "schema": relay.schema, - "secret": secret, - "info": { - "name": relay.name, - "icon": relay.icon, - "pubkey": relay.tenant, - "description": relay.description, - }, - "policy": { - "public_join": cfg_bool(cfg, |c| &c.policy, "public_join", false), - "strip_signatures": cfg_bool(cfg, |c| &c.policy, "strip_signatures", false), - }, - "groups": { - "enabled": cfg_bool(cfg, |c| &c.groups, "enabled", true), - "auto_join": cfg_bool(cfg, |c| &c.groups, "auto_join", true), - }, - "push": { - "enabled": cfg_bool(cfg, |c| &c.push, "enabled", true), - }, - "management": { - "enabled": cfg_bool(cfg, |c| &c.management, "enabled", true), - }, - "blossom": { - "enabled": cfg_bool(cfg, |c| &c.blossom, "enabled", blossom_default), - }, - "roles": { - "member": { "pubkeys": [], "can_invite": true, "can_manage": false } - }, - }) -} - -/// Builds the partial zooid patch payload for relay updates (PATCH). -fn build_patch(relay: &Relay) -> Value { - let blossom_default = relay.plan != "free"; - let cfg = relay.config.as_ref(); - - json!({ - "info": { - "name": relay.name, - "icon": relay.icon, - "description": relay.description, - }, - "policy": { - "public_join": cfg_bool(cfg, |c| &c.policy, "public_join", false), - "strip_signatures": cfg_bool(cfg, |c| &c.policy, "strip_signatures", false), - }, - "groups": { - "enabled": cfg_bool(cfg, |c| &c.groups, "enabled", true), - "auto_join": cfg_bool(cfg, |c| &c.groups, "auto_join", true), - }, - "push": { - "enabled": cfg_bool(cfg, |c| &c.push, "enabled", true), - }, - "management": { - "enabled": cfg_bool(cfg, |c| &c.management, "enabled", true), - }, - "blossom": { - "enabled": cfg_bool(cfg, |c| &c.blossom, "enabled", blossom_default), - }, - }) -} - fn cfg_bool( cfg: Option<&RelayConfig>, section: impl Fn(&RelayConfig) -> &Option, diff --git a/backend/src/repo.rs b/backend/src/repo.rs index 9c85525..e94cf83 100644 --- a/backend/src/repo.rs +++ b/backend/src/repo.rs @@ -13,7 +13,6 @@ fn relay_from_row(row: sqlx::sqlite::SqliteRow) -> Relay { tenant: row.get("tenant"), name: row.get("name"), subdomain: row.get("subdomain"), - schema: row.get("schema"), icon: row.get("icon"), description: row.get("description"), plan: row.get("plan"), @@ -94,12 +93,11 @@ impl Repo { pub async fn upsert_relay(&self, relay: &Relay) -> Result<()> { let config_json = relay.config.as_ref().map(serde_json::to_string).transpose()?; sqlx::query( - "INSERT INTO relays (id, tenant, name, subdomain, schema, icon, description, plan, status, config) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "INSERT INTO relays (id, tenant, name, subdomain, icon, description, plan, status, config) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name, subdomain = excluded.subdomain, - schema = excluded.schema, icon = excluded.icon, description = excluded.description, plan = excluded.plan, @@ -110,7 +108,6 @@ impl Repo { .bind(&relay.tenant) .bind(&relay.name) .bind(&relay.subdomain) - .bind(&relay.schema) .bind(&relay.icon) .bind(&relay.description) .bind(&relay.plan) @@ -142,7 +139,7 @@ impl Repo { pub async fn get_relay(&self, id: &str) -> Result> { let row = sqlx::query( - "SELECT id, tenant, name, subdomain, schema, icon, description, plan, status, config FROM relays WHERE id = ?", + "SELECT id, tenant, name, subdomain, icon, description, plan, status, config FROM relays WHERE id = ?", ) .bind(id) .fetch_optional(&self.pool) @@ -152,7 +149,7 @@ impl Repo { pub async fn list_relays_by_tenant(&self, tenant: &str) -> Result> { let rows = sqlx::query( - "SELECT id, tenant, name, subdomain, schema, icon, description, plan, status, config FROM relays WHERE tenant = ? ORDER BY name", + "SELECT id, tenant, name, subdomain, icon, description, plan, status, config FROM relays WHERE tenant = ? ORDER BY name", ) .bind(tenant) .fetch_all(&self.pool) @@ -162,7 +159,7 @@ impl Repo { pub async fn list_relays(&self) -> Result> { let rows = sqlx::query( - "SELECT id, tenant, name, subdomain, schema, icon, description, plan, status, config FROM relays ORDER BY name", + "SELECT id, tenant, name, subdomain, icon, description, plan, status, config FROM relays ORDER BY name", ) .fetch_all(&self.pool) .await?; diff --git a/justfile b/justfile index 138d59e..2ca7edc 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,6 @@ dev: #!/usr/bin/env sh trap 'kill 0' EXIT - cd backend && RUST_LOG=backend=info cargo run & + cd backend && onchange src -ik -- bash -c 'RUST_LOG=backend=info cargo run' & cd frontend && bun dev & wait