forked from coracle/caravel
Stabilize id/schema
This commit is contained in:
@@ -1,17 +1,19 @@
|
|||||||
CREATE TABLE IF NOT EXISTS tenants (
|
CREATE TABLE IF NOT EXISTS tenants (
|
||||||
pubkey TEXT PRIMARY KEY,
|
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 (
|
CREATE TABLE IF NOT EXISTS relays (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
tenant TEXT NOT NULL,
|
tenant TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
subdomain TEXT NOT NULL,
|
subdomain TEXT NOT NULL UNIQUE,
|
||||||
schema TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
plan TEXT NOT NULL,
|
plan TEXT NOT NULL,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
|
icon TEXT NOT NULL DEFAULT "",
|
||||||
|
config TEXT,
|
||||||
FOREIGN KEY (tenant) REFERENCES tenants(pubkey)
|
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},
|
routing::{get, post, put},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::auth::verify_nip98;
|
use crate::auth::verify_nip98;
|
||||||
use crate::models::{NewTenant, Relay, RelayConfig};
|
use crate::models::{NewTenant, Relay, RelayConfig};
|
||||||
@@ -242,12 +241,12 @@ async fn create_tenant_relay(
|
|||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let id = payload.subdomain.replace('-', "_");
|
||||||
let relay = Relay {
|
let relay = Relay {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: id.clone(),
|
||||||
tenant: pubkey.clone(),
|
tenant: pubkey.clone(),
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
subdomain: payload.subdomain.clone(),
|
subdomain: payload.subdomain.clone(),
|
||||||
schema: payload.subdomain.replace('-', "_"),
|
|
||||||
icon: payload.icon,
|
icon: payload.icon,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
plan: payload.plan,
|
plan: payload.plan,
|
||||||
@@ -275,7 +274,7 @@ async fn create_tenant_relay(
|
|||||||
.into_response();
|
.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");
|
tracing::error!(relay_id = relay.id, error = %err, "zooid create failed");
|
||||||
let _ = state.repo.update_relay_status(&relay.id, "provisioning_failed").await;
|
let _ = state.repo.update_relay_status(&relay.id, "provisioning_failed").await;
|
||||||
return (
|
return (
|
||||||
@@ -362,7 +361,6 @@ async fn update_tenant_relay(
|
|||||||
tenant: existing.tenant,
|
tenant: existing.tenant,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
subdomain: payload.subdomain.clone(),
|
subdomain: payload.subdomain.clone(),
|
||||||
schema: payload.subdomain.replace('-', "_"),
|
|
||||||
icon: payload.icon,
|
icon: payload.icon,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
plan: payload.plan,
|
plan: payload.plan,
|
||||||
@@ -390,15 +388,17 @@ async fn update_tenant_relay(
|
|||||||
.into_response();
|
.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");
|
tracing::error!(relay_id = relay.id, error = %err, "zooid patch failed");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
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();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = state.repo.update_relay_status(&relay.id, "active").await;
|
||||||
|
|
||||||
(StatusCode::OK, Json(relay)).into_response()
|
(StatusCode::OK, Json(relay)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,7 +758,6 @@ async fn admin_update_relay(
|
|||||||
tenant: existing.tenant,
|
tenant: existing.tenant,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
subdomain: payload.subdomain.clone(),
|
subdomain: payload.subdomain.clone(),
|
||||||
schema: payload.subdomain.replace('-', "_"),
|
|
||||||
icon: payload.icon,
|
icon: payload.icon,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
plan: payload.plan,
|
plan: payload.plan,
|
||||||
@@ -786,7 +785,7 @@ async fn admin_update_relay(
|
|||||||
.into_response();
|
.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");
|
tracing::error!(relay_id = relay.id, error = %err, "zooid patch failed");
|
||||||
return (
|
return (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -846,5 +845,3 @@ async fn admin_deactivate_relay(
|
|||||||
|
|
||||||
(StatusCode::OK, Json(relay)).into_response()
|
(StatusCode::OK, Json(relay)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ pub struct Relay {
|
|||||||
pub tenant: String,
|
pub tenant: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub subdomain: String,
|
pub subdomain: String,
|
||||||
pub schema: String,
|
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub plan: String,
|
pub plan: String,
|
||||||
|
|||||||
+60
-66
@@ -2,7 +2,6 @@ use anyhow::{Result, anyhow};
|
|||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use nostr_sdk::nostr::Keys;
|
use nostr_sdk::nostr::Keys;
|
||||||
@@ -36,70 +35,19 @@ 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).
|
/// POSTs the full config (including a generated secret and host).
|
||||||
/// On update, PATCHes only the mutable fields (info + config sections).
|
pub async fn create_relay(&self, relay: &Relay) -> Result<()> {
|
||||||
pub async fn sync_relay(&self, relay: &Relay, is_new: bool) -> Result<()> {
|
|
||||||
let url = format!("{}/relay/{}", self.base_url.trim_end_matches('/'), relay.id);
|
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 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?;
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build_auth_header(&self, url: &str, method: HttpMethod) -> Result<String> {
|
|
||||||
let url = Url::parse(url)?;
|
|
||||||
let data = HttpData::new(url, method);
|
|
||||||
let header = data.to_authorization(&self.admin_keys).await?;
|
|
||||||
Ok(header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 blossom_default = relay.plan != "free";
|
||||||
let cfg = relay.config.as_ref();
|
let cfg = relay.config.as_ref();
|
||||||
|
let host = format!("{}.{}", relay.subdomain, self.relay_domain);
|
||||||
json!({
|
let secret = generate_secret_hex();
|
||||||
|
let payload = json!({
|
||||||
"host": host,
|
"host": host,
|
||||||
"schema": relay.schema,
|
"schema": relay.id,
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
"info": {
|
"info": {
|
||||||
"name": relay.name,
|
"name": relay.name,
|
||||||
@@ -127,15 +75,36 @@ fn build_full_config(relay: &Relay, host: String, secret: String) -> Value {
|
|||||||
"roles": {
|
"roles": {
|
||||||
"member": { "pubkeys": [], "can_invite": true, "can_manage": false }
|
"member": { "pubkeys": [], "can_invite": true, "can_manage": false }
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
let auth = self.build_auth_header(&url, HttpMethod::POST).await?;
|
||||||
|
|
||||||
/// Builds the partial zooid patch payload for relay updates (PATCH).
|
let res = self
|
||||||
fn build_patch(relay: &Relay) -> Value {
|
.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));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 blossom_default = relay.plan != "free";
|
||||||
let cfg = relay.config.as_ref();
|
let cfg = relay.config.as_ref();
|
||||||
|
let patch = json!({
|
||||||
json!({
|
"host": host,
|
||||||
"info": {
|
"info": {
|
||||||
"name": relay.name,
|
"name": relay.name,
|
||||||
"icon": relay.icon,
|
"icon": relay.icon,
|
||||||
@@ -158,7 +127,32 @@ fn build_patch(relay: &Relay) -> Value {
|
|||||||
"blossom": {
|
"blossom": {
|
||||||
"enabled": cfg_bool(cfg, |c| &c.blossom, "enabled", blossom_default),
|
"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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_auth_header(&self, url: &str, method: HttpMethod) -> Result<String> {
|
||||||
|
let url = Url::parse(url)?;
|
||||||
|
let data = HttpData::new(url, method);
|
||||||
|
let header = data.to_authorization(&self.admin_keys).await?;
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cfg_bool(
|
fn cfg_bool(
|
||||||
|
|||||||
+5
-8
@@ -13,7 +13,6 @@ fn relay_from_row(row: sqlx::sqlite::SqliteRow) -> Relay {
|
|||||||
tenant: row.get("tenant"),
|
tenant: row.get("tenant"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
subdomain: row.get("subdomain"),
|
subdomain: row.get("subdomain"),
|
||||||
schema: row.get("schema"),
|
|
||||||
icon: row.get("icon"),
|
icon: row.get("icon"),
|
||||||
description: row.get("description"),
|
description: row.get("description"),
|
||||||
plan: row.get("plan"),
|
plan: row.get("plan"),
|
||||||
@@ -94,12 +93,11 @@ impl Repo {
|
|||||||
pub async fn upsert_relay(&self, relay: &Relay) -> Result<()> {
|
pub async fn upsert_relay(&self, relay: &Relay) -> Result<()> {
|
||||||
let config_json = relay.config.as_ref().map(serde_json::to_string).transpose()?;
|
let config_json = relay.config.as_ref().map(serde_json::to_string).transpose()?;
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO relays (id, tenant, name, subdomain, schema, icon, description, plan, status, config)
|
"INSERT INTO relays (id, tenant, name, subdomain, icon, description, plan, status, config)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
name = excluded.name,
|
name = excluded.name,
|
||||||
subdomain = excluded.subdomain,
|
subdomain = excluded.subdomain,
|
||||||
schema = excluded.schema,
|
|
||||||
icon = excluded.icon,
|
icon = excluded.icon,
|
||||||
description = excluded.description,
|
description = excluded.description,
|
||||||
plan = excluded.plan,
|
plan = excluded.plan,
|
||||||
@@ -110,7 +108,6 @@ impl Repo {
|
|||||||
.bind(&relay.tenant)
|
.bind(&relay.tenant)
|
||||||
.bind(&relay.name)
|
.bind(&relay.name)
|
||||||
.bind(&relay.subdomain)
|
.bind(&relay.subdomain)
|
||||||
.bind(&relay.schema)
|
|
||||||
.bind(&relay.icon)
|
.bind(&relay.icon)
|
||||||
.bind(&relay.description)
|
.bind(&relay.description)
|
||||||
.bind(&relay.plan)
|
.bind(&relay.plan)
|
||||||
@@ -142,7 +139,7 @@ impl Repo {
|
|||||||
|
|
||||||
pub async fn get_relay(&self, id: &str) -> Result<Option<Relay>> {
|
pub async fn get_relay(&self, id: &str) -> Result<Option<Relay>> {
|
||||||
let row = sqlx::query(
|
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)
|
.bind(id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
@@ -152,7 +149,7 @@ impl Repo {
|
|||||||
|
|
||||||
pub async fn list_relays_by_tenant(&self, tenant: &str) -> Result<Vec<Relay>> {
|
pub async fn list_relays_by_tenant(&self, tenant: &str) -> Result<Vec<Relay>> {
|
||||||
let rows = sqlx::query(
|
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)
|
.bind(tenant)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
@@ -162,7 +159,7 @@ impl Repo {
|
|||||||
|
|
||||||
pub async fn list_relays(&self) -> Result<Vec<Relay>> {
|
pub async fn list_relays(&self) -> Result<Vec<Relay>> {
|
||||||
let rows = sqlx::query(
|
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)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
dev:
|
dev:
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
trap 'kill 0' EXIT
|
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 &
|
cd frontend && bun dev &
|
||||||
wait
|
wait
|
||||||
|
|||||||
Reference in New Issue
Block a user