forked from coracle/caravel
Lint, format
This commit is contained in:
+106
-21
@@ -1,7 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use base64::Engine;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
@@ -9,6 +8,7 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
};
|
};
|
||||||
|
use base64::Engine;
|
||||||
use nostr_sdk::{Event, JsonUtil, Kind};
|
use nostr_sdk::{Event, JsonUtil, Kind};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||||
@@ -86,7 +86,8 @@ impl Api {
|
|||||||
.with_state(state)
|
.with_state(state)
|
||||||
.layer(self.cors_layer());
|
.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?;
|
axum::serve(listener, app).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -162,13 +163,18 @@ fn prepare_relay(mut relay: Relay) -> anyhow::Result<Relay> {
|
|||||||
if relay.status.is_empty() {
|
if relay.status.is_empty() {
|
||||||
relay.status = "new".to_string();
|
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_public_join = parse_bool_default(relay.policy_public_join, 0);
|
||||||
relay.policy_strip_signatures = parse_bool_default(relay.policy_strip_signatures, 0);
|
relay.policy_strip_signatures = parse_bool_default(relay.policy_strip_signatures, 0);
|
||||||
relay.groups_enabled = parse_bool_default(relay.groups_enabled, 1);
|
relay.groups_enabled = parse_bool_default(relay.groups_enabled, 1);
|
||||||
relay.management_enabled = parse_bool_default(relay.management_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.blossom_enabled = parse_bool_default(
|
||||||
relay.livekit_enabled = parse_bool_default(relay.livekit_enabled, if relay.plan == "free" { 0 } else { 1 });
|
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);
|
relay.push_enabled = parse_bool_default(relay.push_enabled, 1);
|
||||||
|
|
||||||
Ok(relay)
|
Ok(relay)
|
||||||
@@ -192,7 +198,12 @@ fn auth_fail_response(e: anyhow::Error) -> Response {
|
|||||||
err(StatusCode::UNAUTHORIZED, "unauthorized", &e.to_string())
|
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
|
let auth = headers
|
||||||
.get(axum::http::header::AUTHORIZATION)
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
@@ -304,7 +315,11 @@ async fn list_tenants(
|
|||||||
}
|
}
|
||||||
match state.api.repo.list_tenants().await {
|
match state.api.repo.list_tenants().await {
|
||||||
Ok(tenants) => ok(StatusCode::OK, tenants),
|
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 {
|
match state.api.repo.get_tenant(&pubkey).await {
|
||||||
Ok(Some(tenant)) => ok(StatusCode::OK, tenant),
|
Ok(Some(tenant)) => ok(StatusCode::OK, tenant),
|
||||||
Ok(None) => err(StatusCode::NOT_FOUND, "not-found", "tenant not found"),
|
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),
|
Ok(()) => ok(StatusCode::CREATED, tenant),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if matches!(map_unique_error(&e), Some("pubkey-exists")) {
|
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 {
|
} 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) {
|
let tenant_filter = if state.api.is_admin(&auth) {
|
||||||
query.tenant.as_deref()
|
query.tenant.as_deref()
|
||||||
} else {
|
} 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");
|
return err(StatusCode::FORBIDDEN, "forbidden", "tenant required");
|
||||||
}
|
}
|
||||||
if query.tenant.is_some() {
|
if query.tenant.is_some() {
|
||||||
@@ -389,7 +424,11 @@ async fn list_relays(
|
|||||||
|
|
||||||
match state.api.repo.list_relays(tenant_filter).await {
|
match state.api.repo.list_relays(tenant_filter).await {
|
||||||
Ok(relays) => ok(StatusCode::OK, relays),
|
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 {
|
let relay = match state.api.repo.get_relay(&id).await {
|
||||||
Ok(Some(r)) => r,
|
Ok(Some(r)) => r,
|
||||||
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
|
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)) {
|
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
|
||||||
@@ -482,7 +527,11 @@ async fn create_relay(
|
|||||||
"subdomain already exists",
|
"subdomain already exists",
|
||||||
)
|
)
|
||||||
} else {
|
} 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 {
|
let mut relay = match state.api.repo.get_relay(&id).await {
|
||||||
Ok(Some(r)) => r,
|
Ok(Some(r)) => r,
|
||||||
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
|
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)) {
|
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
|
||||||
@@ -576,7 +631,11 @@ async fn update_relay(
|
|||||||
"subdomain already exists",
|
"subdomain already exists",
|
||||||
)
|
)
|
||||||
} else {
|
} 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 {
|
let relay = match state.api.repo.get_relay(&id).await {
|
||||||
Ok(Some(r)) => r,
|
Ok(Some(r)) => r,
|
||||||
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
|
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)) {
|
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 {
|
match state.api.repo.deactivate_relay(&relay).await {
|
||||||
Ok(()) => ok(StatusCode::OK, ()),
|
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) {
|
let tenant_filter = if state.api.is_admin(&auth) {
|
||||||
query.tenant.as_deref()
|
query.tenant.as_deref()
|
||||||
} else {
|
} 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");
|
return err(StatusCode::FORBIDDEN, "forbidden", "tenant required");
|
||||||
}
|
}
|
||||||
if query.tenant.is_some() {
|
if query.tenant.is_some() {
|
||||||
@@ -640,7 +717,11 @@ async fn list_invoices(
|
|||||||
|
|
||||||
match state.api.repo.list_invoices(tenant_filter).await {
|
match state.api.repo.list_invoices(tenant_filter).await {
|
||||||
Ok(invoices) => ok(StatusCode::OK, invoices),
|
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
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => ok(StatusCode::OK, payload),
|
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
@@ -42,7 +42,10 @@ impl Billing {
|
|||||||
let since = *since_guard;
|
let since = *since_guard;
|
||||||
let activity = self.repo.list_activity(&since, None).await?;
|
let activity = self.repo.list_activity(&since, None).await?;
|
||||||
for a in &activity {
|
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?;
|
self.maybe_reset_anchor_for_first_paid_relay(a).await?;
|
||||||
}
|
}
|
||||||
*since_guard = (*since_guard).max(a.created_at);
|
*since_guard = (*since_guard).max(a.created_at);
|
||||||
@@ -83,7 +86,12 @@ impl Billing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_invoice_if_due(&self, tenant: &Tenant) -> Result<()> {
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +112,16 @@ impl Billing {
|
|||||||
return Ok(());
|
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 invoice_id = uuid::Uuid::new_v4().to_string();
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
||||||
for relay in active_paid_relays {
|
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 {
|
if hours <= 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -178,7 +190,9 @@ impl Billing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut collected = false;
|
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?;
|
self.repo.mark_invoice_paid(&invoice.id).await?;
|
||||||
collected = true;
|
collected = true;
|
||||||
}
|
}
|
||||||
@@ -256,7 +270,7 @@ impl Billing {
|
|||||||
uri: &nostr_sdk::nips::nip47::NostrWalletConnectURI,
|
uri: &nostr_sdk::nips::nip47::NostrWalletConnectURI,
|
||||||
request: nostr_sdk::nips::nip47::Request,
|
request: nostr_sdk::nips::nip47::Request,
|
||||||
) -> Result<nostr_sdk::nips::nip47::Response> {
|
) -> 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_keys = Keys::new(uri.secret.clone());
|
||||||
let app_pubkey = app_keys.public_key();
|
let app_pubkey = app_keys.public_key();
|
||||||
|
|||||||
+39
-33
@@ -1,7 +1,11 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::Result;
|
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};
|
use crate::models::{Activity, Invoice, InvoiceItem, Relay, Tenant};
|
||||||
|
|
||||||
@@ -12,8 +16,10 @@ pub struct Repo {
|
|||||||
|
|
||||||
impl Repo {
|
impl Repo {
|
||||||
pub async fn new() -> Result<Self> {
|
pub async fn new() -> Result<Self> {
|
||||||
let database_url = std::env::var("DATABASE_URL")
|
let raw_database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||||
.unwrap_or_else(|_| "sqlite://data/caravel.db".to_string());
|
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://")
|
if let Some(path) = database_url.strip_prefix("sqlite://")
|
||||||
&& !path.is_empty()
|
&& !path.is_empty()
|
||||||
@@ -24,9 +30,11 @@ impl Repo {
|
|||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let connect_options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.connect(&database_url)
|
.connect_with(connect_options)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query("PRAGMA journal_mode = WAL;")
|
sqlx::query("PRAGMA journal_mode = WAL;")
|
||||||
@@ -118,7 +126,11 @@ impl Repo {
|
|||||||
Ok(())
|
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?;
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
sqlx::query("UPDATE tenant SET billing_anchor = ? WHERE pubkey = ?")
|
sqlx::query("UPDATE tenant SET billing_anchor = ? WHERE pubkey = ?")
|
||||||
@@ -284,20 +296,6 @@ impl Repo {
|
|||||||
Ok(())
|
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<()> {
|
pub async fn fail_relay_sync(&self, relay: &Relay, sync_error: String) -> Result<()> {
|
||||||
let mut tx = self.pool.begin().await?;
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
@@ -313,7 +311,11 @@ impl Repo {
|
|||||||
Ok(())
|
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?;
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -396,7 +398,11 @@ impl Repo {
|
|||||||
Ok(())
|
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?;
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -485,17 +491,6 @@ impl Repo {
|
|||||||
Ok(rows)
|
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> {
|
pub async fn total_pending_invoices_for_tenant(&self, tenant: &str) -> Result<i64> {
|
||||||
let count = sqlx::query_scalar::<_, i64>(
|
let count = sqlx::query_scalar::<_, i64>(
|
||||||
"SELECT COUNT(*) FROM invoice
|
"SELECT COUNT(*) FROM invoice
|
||||||
@@ -516,5 +511,16 @@ impl Repo {
|
|||||||
};
|
};
|
||||||
Ok(sats)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ impl Robot {
|
|||||||
return Err(anyhow!("no outbox relays found for recipient"));
|
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() {
|
if dm_relays.is_empty() {
|
||||||
return Err(anyhow!("no messaging relays found for recipient"));
|
return Err(anyhow!("no messaging relays found for recipient"));
|
||||||
}
|
}
|
||||||
@@ -123,7 +125,9 @@ impl Robot {
|
|||||||
client.add_relay(relay).await?;
|
client.add_relay(relay).await?;
|
||||||
}
|
}
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
client.send_private_msg(recipient_pubkey, message, []).await?;
|
client
|
||||||
|
.send_private_msg(recipient_pubkey, message, [])
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +139,7 @@ impl Robot {
|
|||||||
let pubkey = PublicKey::parse(recipient)?;
|
let pubkey = PublicKey::parse(recipient)?;
|
||||||
let client = indexer_client(&self.secret, &self.indexer_relays).await?;
|
let client = indexer_client(&self.secret, &self.indexer_relays).await?;
|
||||||
let filter = Filter::new().author(pubkey).kind(Kind::Custom(10002));
|
let filter = Filter::new().author(pubkey).kind(Kind::Custom(10002));
|
||||||
let events = client
|
let events = client.fetch_events(filter, Duration::from_secs(5)).await?;
|
||||||
.fetch_events(filter, Duration::from_secs(5))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut relays = Vec::new();
|
let mut relays = Vec::new();
|
||||||
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
|
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
|
||||||
@@ -171,9 +173,7 @@ impl Robot {
|
|||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
|
||||||
let filter = Filter::new().author(pubkey).kind(Kind::Custom(10050));
|
let filter = Filter::new().author(pubkey).kind(Kind::Custom(10050));
|
||||||
let events = client
|
let events = client.fetch_events(filter, Duration::from_secs(5)).await?;
|
||||||
.fetch_events(filter, Duration::from_secs(5))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut relays = Vec::new();
|
let mut relays = Vec::new();
|
||||||
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
|
if let Some(event) = events.into_iter().max_by_key(|e| e.created_at) {
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ export default function Account() {
|
|||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h2 class="text-lg font-semibold text-gray-900">Account Status</h2>
|
<h2 class="text-lg font-semibold text-gray-900">Account Status</h2>
|
||||||
<Show when={tenant()}>
|
<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">
|
||||||
<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
|
||||||
tenant
|
</span>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -30,13 +30,11 @@ export default function AdminTenantDetail() {
|
|||||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<h2 class="text-lg font-semibold mb-4">Status</h2>
|
<h2 class="text-lg font-semibold mb-4">Status</h2>
|
||||||
<Show when={detail()}>
|
<Show when={detail()}>
|
||||||
{(d) => (
|
<div class="space-y-3">
|
||||||
<div class="space-y-3">
|
<p class="text-sm text-gray-700">
|
||||||
<p class="text-sm text-gray-700">
|
Current: <span class="font-medium uppercase tracking-wide">tenant</span>
|
||||||
Current: <span class="font-medium uppercase tracking-wide">tenant</span>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ dev:
|
|||||||
cd frontend && bun dev &
|
cd frontend && bun dev &
|
||||||
wait
|
wait
|
||||||
|
|
||||||
|
dev-frontend:
|
||||||
|
cd frontend && bun run dev
|
||||||
|
|
||||||
|
build-frontend:
|
||||||
|
cd frontend && bun run build
|
||||||
|
|
||||||
|
preview-frontend:
|
||||||
|
cd frontend && bun run preview
|
||||||
|
|
||||||
fmt-backend:
|
fmt-backend:
|
||||||
cd backend && cargo fmt
|
cd backend && cargo fmt
|
||||||
|
|
||||||
@@ -18,6 +27,6 @@ lint: lint-backend
|
|||||||
build-backend:
|
build-backend:
|
||||||
cd backend && cargo build
|
cd backend && cargo build
|
||||||
|
|
||||||
build: build-backend
|
build: build-backend build-frontend
|
||||||
|
|
||||||
check: fmt lint build
|
check: fmt lint build
|
||||||
|
|||||||
Reference in New Issue
Block a user