Lint, format

This commit is contained in:
Jon Staab
2026-03-26 10:56:42 -07:00
parent 5c06070913
commit 087405b1ac
7 changed files with 191 additions and 81 deletions
+106 -21
View File
@@ -1,7 +1,6 @@
use std::sync::Arc;
use anyhow::{Result, anyhow};
use base64::Engine;
use axum::{
Json, Router,
extract::{Path, Query, State},
@@ -9,6 +8,7 @@ use axum::{
response::{IntoResponse, Response},
routing::{get, post, put},
};
use base64::Engine;
use nostr_sdk::{Event, JsonUtil, Kind};
use serde::{Deserialize, Serialize};
use tower_http::cors::{AllowOrigin, CorsLayer};
@@ -86,7 +86,8 @@ impl Api {
.with_state(state)
.layer(self.cors_layer());
let listener = tokio::net::TcpListener::bind(format!("{}:{}", self.host, self.port)).await?;
let listener =
tokio::net::TcpListener::bind(format!("{}:{}", self.host, self.port)).await?;
axum::serve(listener, app).await?;
Ok(())
}
@@ -162,13 +163,18 @@ fn prepare_relay(mut relay: Relay) -> anyhow::Result<Relay> {
if relay.status.is_empty() {
relay.status = "new".to_string();
}
relay.sync_error = relay.sync_error;
relay.policy_public_join = parse_bool_default(relay.policy_public_join, 0);
relay.policy_strip_signatures = parse_bool_default(relay.policy_strip_signatures, 0);
relay.groups_enabled = parse_bool_default(relay.groups_enabled, 1);
relay.management_enabled = parse_bool_default(relay.management_enabled, 1);
relay.blossom_enabled = parse_bool_default(relay.blossom_enabled, if relay.plan == "free" { 0 } else { 1 });
relay.livekit_enabled = parse_bool_default(relay.livekit_enabled, if relay.plan == "free" { 0 } else { 1 });
relay.blossom_enabled = parse_bool_default(
relay.blossom_enabled,
if relay.plan == "free" { 0 } else { 1 },
);
relay.livekit_enabled = parse_bool_default(
relay.livekit_enabled,
if relay.plan == "free" { 0 } else { 1 },
);
relay.push_enabled = parse_bool_default(relay.push_enabled, 1);
Ok(relay)
@@ -192,7 +198,12 @@ fn auth_fail_response(e: anyhow::Error) -> Response {
err(StatusCode::UNAUTHORIZED, "unauthorized", &e.to_string())
}
fn extract_auth_pubkey(headers: &HeaderMap, method: &Method, _uri: &Uri, host: &str) -> Result<String> {
fn extract_auth_pubkey(
headers: &HeaderMap,
method: &Method,
_uri: &Uri,
host: &str,
) -> Result<String> {
let auth = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
@@ -304,7 +315,11 @@ async fn list_tenants(
}
match state.api.repo.list_tenants().await {
Ok(tenants) => ok(StatusCode::OK, tenants),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
@@ -325,7 +340,11 @@ async fn get_tenant(
match state.api.repo.get_tenant(&pubkey).await {
Ok(Some(tenant)) => ok(StatusCode::OK, tenant),
Ok(None) => err(StatusCode::NOT_FOUND, "not-found", "tenant not found"),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
@@ -351,9 +370,17 @@ async fn create_tenant(
Ok(()) => ok(StatusCode::CREATED, tenant),
Err(e) => {
if matches!(map_unique_error(&e), Some("pubkey-exists")) {
err(StatusCode::UNPROCESSABLE_ENTITY, "pubkey-exists", "tenant already exists")
err(
StatusCode::UNPROCESSABLE_ENTITY,
"pubkey-exists",
"tenant already exists",
)
} else {
err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string())
err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)
}
}
}
@@ -374,7 +401,15 @@ async fn list_relays(
let tenant_filter = if state.api.is_admin(&auth) {
query.tenant.as_deref()
} else {
if state.api.repo.get_tenant(&auth).await.ok().flatten().is_none() {
if state
.api
.repo
.get_tenant(&auth)
.await
.ok()
.flatten()
.is_none()
{
return err(StatusCode::FORBIDDEN, "forbidden", "tenant required");
}
if query.tenant.is_some() {
@@ -389,7 +424,11 @@ async fn list_relays(
match state.api.repo.list_relays(tenant_filter).await {
Ok(relays) => ok(StatusCode::OK, relays),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
@@ -408,7 +447,13 @@ async fn get_relay(
let relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => {
return err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
@@ -482,7 +527,11 @@ async fn create_relay(
"subdomain already exists",
)
} else {
err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string())
err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)
}
}
}
@@ -504,7 +553,13 @@ async fn update_relay(
let mut relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => {
return err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
@@ -576,7 +631,11 @@ async fn update_relay(
"subdomain already exists",
)
} else {
err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string())
err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)
}
}
}
@@ -597,7 +656,13 @@ async fn deactivate_relay(
let relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => {
return err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
@@ -606,7 +671,11 @@ async fn deactivate_relay(
match state.api.repo.deactivate_relay(&relay).await {
Ok(()) => ok(StatusCode::OK, ()),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
@@ -625,7 +694,15 @@ async fn list_invoices(
let tenant_filter = if state.api.is_admin(&auth) {
query.tenant.as_deref()
} else {
if state.api.repo.get_tenant(&auth).await.ok().flatten().is_none() {
if state
.api
.repo
.get_tenant(&auth)
.await
.ok()
.flatten()
.is_none()
{
return err(StatusCode::FORBIDDEN, "forbidden", "tenant required");
}
if query.tenant.is_some() {
@@ -640,7 +717,11 @@ async fn list_invoices(
match state.api.repo.list_invoices(tenant_filter).await {
Ok(invoices) => ok(StatusCode::OK, invoices),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
@@ -668,6 +749,10 @@ async fn update_tenant_billing(
.await
{
Ok(()) => ok(StatusCode::OK, payload),
Err(e) => err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string()),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
+20 -6
View File
@@ -42,7 +42,10 @@ impl Billing {
let since = *since_guard;
let activity = self.repo.list_activity(&since, None).await?;
for a in &activity {
if matches!(a.activity_type.as_str(), "create_relay" | "update_relay" | "activate_relay") {
if matches!(
a.activity_type.as_str(),
"create_relay" | "update_relay" | "activate_relay"
) {
self.maybe_reset_anchor_for_first_paid_relay(a).await?;
}
*since_guard = (*since_guard).max(a.created_at);
@@ -83,7 +86,12 @@ impl Billing {
}
async fn generate_invoice_if_due(&self, tenant: &Tenant) -> Result<()> {
if self.repo.total_pending_invoices_for_tenant(&tenant.pubkey).await? > 0 {
if self
.repo
.total_pending_invoices_for_tenant(&tenant.pubkey)
.await?
> 0
{
return Ok(());
}
@@ -104,12 +112,16 @@ impl Billing {
return Ok(());
}
let usage_events = self.repo.list_activity(&tenant.billing_anchor, Some(&tenant.pubkey)).await?;
let usage_events = self
.repo
.list_activity(&tenant.billing_anchor, Some(&tenant.pubkey))
.await?;
let invoice_id = uuid::Uuid::new_v4().to_string();
let mut items = Vec::new();
for relay in active_paid_relays {
let hours = relay_active_hours_in_window(&relay, &usage_events, period_start, period_end);
let hours =
relay_active_hours_in_window(&relay, &usage_events, period_start, period_end);
if hours <= 0 {
continue;
}
@@ -178,7 +190,9 @@ impl Billing {
}
let mut collected = false;
if !tenant.nwc_url.trim().is_empty() && self.pay_invoice_nwc(&tenant.nwc_url, &invoice.bolt11).await {
if !tenant.nwc_url.trim().is_empty()
&& self.pay_invoice_nwc(&tenant.nwc_url, &invoice.bolt11).await
{
self.repo.mark_invoice_paid(&invoice.id).await?;
collected = true;
}
@@ -256,7 +270,7 @@ impl Billing {
uri: &nostr_sdk::nips::nip47::NostrWalletConnectURI,
request: nostr_sdk::nips::nip47::Request,
) -> Result<nostr_sdk::nips::nip47::Response> {
use nostr_sdk::{Client, Filter, Kind, Keys, Timestamp};
use nostr_sdk::{Client, Filter, Keys, Kind, Timestamp};
let app_keys = Keys::new(uri.secret.clone());
let app_pubkey = app_keys.public_key();
+39 -33
View File
@@ -1,7 +1,11 @@
use std::path::Path;
use std::str::FromStr;
use anyhow::Result;
use sqlx::{Sqlite, SqlitePool, Transaction, sqlite::SqlitePoolOptions};
use sqlx::{
Sqlite, SqlitePool, Transaction,
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
};
use crate::models::{Activity, Invoice, InvoiceItem, Relay, Tenant};
@@ -12,8 +16,10 @@ pub struct Repo {
impl Repo {
pub async fn new() -> Result<Self> {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite://data/caravel.db".to_string());
let raw_database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
format!("sqlite://{}/data/caravel.db", env!("CARGO_MANIFEST_DIR"))
});
let database_url = normalize_sqlite_url(&raw_database_url);
if let Some(path) = database_url.strip_prefix("sqlite://")
&& !path.is_empty()
@@ -24,9 +30,11 @@ impl Repo {
std::fs::create_dir_all(parent)?;
}
let connect_options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&database_url)
.connect_with(connect_options)
.await?;
sqlx::query("PRAGMA journal_mode = WAL;")
@@ -118,7 +126,11 @@ impl Repo {
Ok(())
}
pub async fn update_tenant_billing_anchor(&self, pubkey: &str, billing_anchor: i64) -> Result<()> {
pub async fn update_tenant_billing_anchor(
&self,
pubkey: &str,
billing_anchor: i64,
) -> Result<()> {
let mut tx = self.pool.begin().await?;
sqlx::query("UPDATE tenant SET billing_anchor = ? WHERE pubkey = ?")
@@ -284,20 +296,6 @@ impl Repo {
Ok(())
}
pub async fn activate_relay(&self, relay: &Relay) -> Result<()> {
let mut tx = self.pool.begin().await?;
sqlx::query("UPDATE relay SET status = 'active' WHERE id = ?")
.bind(&relay.id)
.execute(&mut *tx)
.await?;
Self::insert_activity(&mut tx, "activate_relay", "relay", &relay.id).await?;
tx.commit().await?;
Ok(())
}
pub async fn fail_relay_sync(&self, relay: &Relay, sync_error: String) -> Result<()> {
let mut tx = self.pool.begin().await?;
@@ -313,7 +311,11 @@ impl Repo {
Ok(())
}
pub async fn create_invoice(&self, invoice: &Invoice, invoice_items: &[InvoiceItem]) -> Result<()> {
pub async fn create_invoice(
&self,
invoice: &Invoice,
invoice_items: &[InvoiceItem],
) -> Result<()> {
let mut tx = self.pool.begin().await?;
sqlx::query(
@@ -396,7 +398,11 @@ impl Repo {
Ok(())
}
pub async fn mark_invoice_attempted(&self, invoice_id: &str, error: Option<&str>) -> Result<()> {
pub async fn mark_invoice_attempted(
&self,
invoice_id: &str,
error: Option<&str>,
) -> Result<()> {
let mut tx = self.pool.begin().await?;
sqlx::query(
@@ -485,17 +491,6 @@ impl Repo {
Ok(rows)
}
pub async fn total_active_paid_relays_for_tenant(&self, tenant: &str) -> Result<i64> {
let count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM relay
WHERE tenant = ? AND status = 'active' AND plan != 'free'",
)
.bind(tenant)
.fetch_one(&self.pool)
.await?;
Ok(count)
}
pub async fn total_pending_invoices_for_tenant(&self, tenant: &str) -> Result<i64> {
let count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM invoice
@@ -516,5 +511,16 @@ impl Repo {
};
Ok(sats)
}
}
fn normalize_sqlite_url(url: &str) -> String {
let Some(path) = url.strip_prefix("sqlite://") else {
return url.to_string();
};
if path.is_empty() || path == ":memory:" || Path::new(path).is_absolute() {
return url.to_string();
}
format!("sqlite://{}/{}", env!("CARGO_MANIFEST_DIR"), path)
}
+8 -8
View File
@@ -111,7 +111,9 @@ impl Robot {
return Err(anyhow!("no outbox relays found for recipient"));
}
let dm_relays = self.fetch_messaging_relays_from_outbox(recipient, &outbox).await?;
let dm_relays = self
.fetch_messaging_relays_from_outbox(recipient, &outbox)
.await?;
if dm_relays.is_empty() {
return Err(anyhow!("no messaging relays found for recipient"));
}
@@ -123,7 +125,9 @@ impl Robot {
client.add_relay(relay).await?;
}
client.connect().await;
client.send_private_msg(recipient_pubkey, message, []).await?;
client
.send_private_msg(recipient_pubkey, message, [])
.await?;
Ok(())
}
@@ -135,9 +139,7 @@ impl Robot {
let pubkey = PublicKey::parse(recipient)?;
let client = indexer_client(&self.secret, &self.indexer_relays).await?;
let filter = Filter::new().author(pubkey).kind(Kind::Custom(10002));
let events = client
.fetch_events(filter, Duration::from_secs(5))
.await?;
let events = client.fetch_events(filter, Duration::from_secs(5)).await?;
let mut relays = Vec::new();
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
@@ -171,9 +173,7 @@ impl Robot {
client.connect().await;
let filter = Filter::new().author(pubkey).kind(Kind::Custom(10050));
let events = client
.fetch_events(filter, Duration::from_secs(5))
.await?;
let events = client.fetch_events(filter, Duration::from_secs(5)).await?;
let mut relays = Vec::new();
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
+3 -5
View File
@@ -45,11 +45,9 @@ export default function Account() {
<div class="flex items-center justify-between gap-3">
<h2 class="text-lg font-semibold text-gray-900">Account Status</h2>
<Show when={tenant()}>
{(t) => (
<span class="rounded-full border border-gray-300 bg-gray-100 px-2.5 py-1 text-xs font-medium uppercase tracking-wide text-gray-700">
tenant
</span>
)}
<span class="rounded-full border border-gray-300 bg-gray-100 px-2.5 py-1 text-xs font-medium uppercase tracking-wide text-gray-700">
tenant
</span>
</Show>
</div>
</section>
@@ -30,13 +30,11 @@ export default function AdminTenantDetail() {
<section class="bg-white border border-gray-200 rounded-xl p-6">
<h2 class="text-lg font-semibold mb-4">Status</h2>
<Show when={detail()}>
{(d) => (
<div class="space-y-3">
<p class="text-sm text-gray-700">
Current: <span class="font-medium uppercase tracking-wide">tenant</span>
</p>
</div>
)}
<div class="space-y-3">
<p class="text-sm text-gray-700">
Current: <span class="font-medium uppercase tracking-wide">tenant</span>
</p>
</div>
</Show>
</section>
+10 -1
View File
@@ -5,6 +5,15 @@ dev:
cd frontend && bun dev &
wait
dev-frontend:
cd frontend && bun run dev
build-frontend:
cd frontend && bun run build
preview-frontend:
cd frontend && bun run preview
fmt-backend:
cd backend && cargo fmt
@@ -18,6 +27,6 @@ lint: lint-backend
build-backend:
cd backend && cargo build
build: build-backend
build: build-backend build-frontend
check: fmt lint build