Stabilize id/schema
This commit is contained in:
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user