Stabilize id/schema

This commit is contained in:
Jon Staab
2026-03-03 10:16:09 -08:00
parent 46a270513e
commit 7e577bf7ff
10 changed files with 119 additions and 135 deletions
+5 -3
View File
@@ -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)
);
@@ -1 +0,0 @@
ALTER TABLE relays ADD COLUMN icon TEXT NOT NULL DEFAULT "";
@@ -1 +0,0 @@
ALTER TABLE tenants ADD COLUMN tenant_nwc_url TEXT NOT NULL DEFAULT "";
@@ -1,2 +0,0 @@
CREATE UNIQUE INDEX IF NOT EXISTS relays_subdomain_unique
ON relays (subdomain);
@@ -1 +0,0 @@
ALTER TABLE relays ADD COLUMN config TEXT;
+8 -11
View File
@@ -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()
}
-1
View File
@@ -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,
+100 -106
View File
@@ -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<Value>,
+5 -8
View File
@@ -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<Option<Relay>> {
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<Vec<Relay>> {
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<Vec<Relay>> {
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?;
+1 -1
View File
@@ -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