Refactor error handling
Docker / build-and-push-image (backend, backend, coracle/caravel-backend) (push) Failing after 4m59s
Docker / build-and-push-image (frontend, frontend, coracle/caravel-frontend) (push) Successful in 2m50s

This commit is contained in:
Jon Staab
2026-05-15 10:32:23 -07:00
parent 5590b14074
commit 1c3e0d619a
9 changed files with 256 additions and 443 deletions
+4 -4
View File
@@ -1,10 +1,10 @@
use std::sync::Arc;
use axum::{extract::State, http::StatusCode, response::Response};
use axum::extract::State;
use serde::Serialize;
use crate::api::{Api, AuthedPubkey};
use crate::web::ok;
use crate::web::{ApiResult, ok};
#[derive(Serialize)]
struct IdentityResponse {
@@ -15,7 +15,7 @@ struct IdentityResponse {
pub async fn get_identity(
State(api): State<Arc<Api>>,
AuthedPubkey(pubkey): AuthedPubkey,
) -> Response {
) -> ApiResult {
let is_admin = api.is_admin(&pubkey);
ok(StatusCode::OK, IdentityResponse { pubkey, is_admin })
ok(IdentityResponse { pubkey, is_admin })
}
+21 -47
View File
@@ -1,42 +1,33 @@
use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Response,
};
use axum::extract::{Path, State};
use reqwest::StatusCode;
use crate::api::{Api, AuthedPubkey};
use crate::stripe::InvoiceLookupError;
use crate::web::{ApiError, err, ok};
use crate::web::{ApiError, ApiResult, bad_request, internal, not_found, ok};
pub async fn list_tenant_invoices(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(pubkey): Path<String>,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin_or_tenant(&auth, &pubkey)?;
let tenant = api.get_tenant_or_404(&pubkey).await?;
match api
let invoices = api
.billing
.stripe_list_invoices(&tenant.stripe_customer_id)
.await
{
Ok(invoices) => Ok(ok(StatusCode::OK, invoices)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
.map_err(internal)?;
ok(invoices)
}
pub async fn get_invoice(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
) -> ApiResult {
let (invoice, tenant) = api
.billing
.get_invoice_with_tenant(&id)
@@ -50,14 +41,14 @@ pub async fn get_invoice(
.await
.map_err(map_invoice_lookup_error)?;
Ok(ok(StatusCode::OK, invoice))
ok(invoice)
}
pub async fn get_invoice_bolt11(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
) -> ApiResult {
let (invoice, tenant) = api
.billing
.get_invoice_with_tenant(&id)
@@ -73,46 +64,29 @@ pub async fn get_invoice_bolt11(
let status = invoice["status"].as_str().unwrap_or_default();
if status != "open" {
return Ok(err(
StatusCode::BAD_REQUEST,
"invoice-not-open",
"invoice is not open",
));
return Err(bad_request("invoice-not-open", "invoice is not open"));
}
let amount_due = invoice["amount_due"].as_i64().unwrap_or(0);
let currency = invoice["currency"].as_str().unwrap_or("usd");
match api
let bolt11 = api
.billing
.get_or_create_manual_lightning_bolt11(&id, &tenant.pubkey, amount_due, currency)
.await
{
Ok(bolt11) => Ok(ok(StatusCode::OK, serde_json::json!({ "bolt11": bolt11 }))),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
.map_err(internal)?;
ok(serde_json::json!({ "bolt11": bolt11 }))
}
fn map_invoice_lookup_error(error: InvoiceLookupError) -> ApiError {
match error {
InvoiceLookupError::StripeClient { status } => {
let status = StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::BAD_REQUEST);
match status {
StatusCode::NOT_FOUND => ApiError::NotFound("invoice not found"),
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
ApiError::Forbidden("invoice access denied")
}
_ => ApiError::Client {
status,
code: "invoice-request-rejected",
message: "invoice request rejected",
},
InvoiceLookupError::StripeClient { status } => match status {
StatusCode::NOT_FOUND => not_found("invoice not found"),
_ => {
tracing::warn!(%status, "stripe invoice request returned unexpected status");
internal("invoice request rejected")
}
}
InvoiceLookupError::Internal(error) => ApiError::Internal(error.to_string()),
},
InvoiceLookupError::Internal(error) => internal(error),
}
}
+7 -11
View File
@@ -1,21 +1,17 @@
use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::Response,
};
use axum::extract::{Path, State};
use crate::api::Api;
use crate::web::{err, ok};
use crate::web::{ApiResult, not_found, ok};
pub async fn list_plans(State(api): State<Arc<Api>>) -> Response {
ok(StatusCode::OK, api.query.list_plans())
pub async fn list_plans(State(api): State<Arc<Api>>) -> ApiResult {
ok(api.query.list_plans())
}
pub async fn get_plan(State(api): State<Arc<Api>>, Path(id): Path<String>) -> Response {
pub async fn get_plan(State(api): State<Arc<Api>>, Path(id): Path<String>) -> ApiResult {
match api.query.get_plan(&id) {
Some(plan) => ok(StatusCode::OK, plan),
None => err(StatusCode::NOT_FOUND, "not-found", "plan not found"),
Some(plan) => ok(plan),
None => Err(not_found("plan not found")),
}
}
+68 -209
View File
@@ -4,8 +4,6 @@ use anyhow::Result;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::Response,
};
use serde::Deserialize;
@@ -13,7 +11,10 @@ use crate::api::{Api, AuthedPubkey};
use crate::models::{
RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT, RELAY_STATUS_INACTIVE, Relay,
};
use crate::web::{ApiError, err, map_unique_error, ok, parse_bool_default};
use crate::web::{
ApiError, ApiResult, bad_request, created, internal, map_unique_error, ok,
parse_bool_default, unprocessable,
};
#[derive(Deserialize)]
pub struct CreateRelayRequest {
@@ -51,106 +52,56 @@ pub struct UpdateRelayRequest {
pub async fn list_relays(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin(&auth)?;
match api.query.list_relays().await {
Ok(relays) => Ok(ok(StatusCode::OK, relays)),
Err(e) => Err(ApiError::Internal(e.to_string())),
}
let relays = api.query.list_relays().await.map_err(internal)?;
ok(relays)
}
pub async fn get_relay(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
let relay = match api.query.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?;
Ok(ok(StatusCode::OK, relay))
ok(relay)
}
pub async fn list_relay_activity(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
let relay = match api.query.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?;
match api.query.list_activity_for_relay(&id).await {
Ok(activity) => Ok(ok(
StatusCode::OK,
serde_json::json!({ "activity": activity }),
)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
let activity = api
.query
.list_activity_for_relay(&id)
.await
.map_err(internal)?;
ok(serde_json::json!({ "activity": activity }))
}
pub async fn list_relay_members(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
let relay = match api.query.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?;
match fetch_relay_members(&api, &relay).await {
Ok(members) => Ok(ok(
StatusCode::OK,
serde_json::json!({ "members": members }),
)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
let members = fetch_relay_members(&api, &relay).await.map_err(internal)?;
ok(serde_json::json!({ "members": members }))
}
pub async fn create_relay(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Json(payload): Json<CreateRelayRequest>,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin_or_tenant(&auth, &payload.tenant)?;
let relay_id = format!(
@@ -159,7 +110,7 @@ pub async fn create_relay(
&uuid::Uuid::new_v4().simple().to_string()[..8]
);
let mut relay = Relay {
let relay = Relay {
id: relay_id.clone(),
tenant: payload.tenant,
schema: relay_id.clone(),
@@ -181,31 +132,13 @@ pub async fn create_relay(
synced: 0,
};
relay = match prepare_relay(&api, relay) {
Ok(r) => r,
Err(e) => {
return Ok(relay_validation_error_response(e));
}
};
let relay = prepare_relay(&api, relay).map_err(validation_error)?;
match api.command.create_relay(&relay).await {
Ok(()) => Ok(ok(StatusCode::CREATED, relay)),
Err(e) => {
if matches!(map_unique_error(&e), Some("subdomain-exists")) {
Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"subdomain-exists",
"subdomain already exists",
))
} else {
Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
))
}
}
}
api.command
.create_relay(&relay)
.await
.map_err(map_relay_write_error)?;
created(relay)
}
pub async fn update_relay(
@@ -213,19 +146,8 @@ pub async fn update_relay(
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
Json(payload): Json<UpdateRelayRequest>,
) -> Result<Response, ApiError> {
let mut relay = match api.query.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
) -> ApiResult {
let mut relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?;
let current_plan = relay.plan.clone();
@@ -268,12 +190,7 @@ pub async fn update_relay(
relay.push_enabled = v;
}
relay = match prepare_relay(&api, relay) {
Ok(r) => r,
Err(e) => {
return Ok(relay_validation_error_response(e));
}
};
let relay = prepare_relay(&api, relay).map_err(validation_error)?;
let plan_changed = requested_plan
.as_deref()
@@ -285,123 +202,61 @@ pub async fn update_relay(
.get_plan(&relay.plan)
.expect("validated plan must exist");
if let Some(limit) = selected_plan.members {
let current_members = match fetch_relay_members(&api, &relay).await {
Ok(members) => members.len() as i64,
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
let current_members = fetch_relay_members(&api, &relay)
.await
.map_err(internal)?
.len() as i64;
if current_members > limit {
let message = format!(
"relay has {current_members} members, which exceeds the {} plan limit of {limit}",
selected_plan.name.to_lowercase()
);
return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"member-limit-exceeded",
&message,
));
return Err(unprocessable("member-limit-exceeded", &message));
}
}
}
match api.command.update_relay(&relay).await {
Ok(()) => Ok(ok(StatusCode::OK, relay)),
Err(e) => {
if matches!(map_unique_error(&e), Some("subdomain-exists")) {
Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"subdomain-exists",
"subdomain already exists",
))
} else {
Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
))
}
}
}
api.command
.update_relay(&relay)
.await
.map_err(map_relay_write_error)?;
ok(relay)
}
pub async fn deactivate_relay(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
let relay = match api.query.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?;
if relay.status == RELAY_STATUS_INACTIVE || relay.status == RELAY_STATUS_DELINQUENT {
return Ok(err(
StatusCode::BAD_REQUEST,
"relay-is-inactive",
"relay is already inactive",
));
return Err(bad_request("relay-is-inactive", "relay is already inactive"));
}
match api.command.deactivate_relay(&relay).await {
Ok(()) => Ok(ok(StatusCode::OK, ())),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
api.command
.deactivate_relay(&relay)
.await
.map_err(internal)?;
ok(())
}
pub async fn reactivate_relay(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
let relay = match api.query.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
));
}
};
) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?;
if relay.status == RELAY_STATUS_ACTIVE {
return Ok(err(
StatusCode::BAD_REQUEST,
"relay-is-active",
"relay is already active",
));
return Err(bad_request("relay-is-active", "relay is already active"));
}
match api.command.activate_relay(&relay).await {
Ok(()) => Ok(ok(StatusCode::OK, ())),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
api.command.activate_relay(&relay).await.map_err(internal)?;
ok(())
}
// --- helpers ----------------------------------------------------------------
@@ -447,6 +302,18 @@ fn prepare_relay(api: &Api, mut relay: Relay) -> Result<Relay, RelayValidationEr
Ok(relay)
}
fn validation_error(error: RelayValidationError) -> ApiError {
unprocessable(error.code(), error.message())
}
fn map_relay_write_error(e: anyhow::Error) -> ApiError {
if matches!(map_unique_error(&e), Some("subdomain-exists")) {
unprocessable("subdomain-exists", "subdomain already exists")
} else {
internal(e)
}
}
const SUBDOMAIN_LABEL_MAX_LEN: usize = 63;
const RESERVED_SUBDOMAIN_LABELS: [&str; 2] = ["api", "admin"];
@@ -536,11 +403,3 @@ fn validate_subdomain_label(subdomain: &str) -> Result<(), SubdomainValidationEr
Ok(())
}
fn relay_validation_error_response(error: RelayValidationError) -> Response {
err(
StatusCode::UNPROCESSABLE_ENTITY,
error.code(),
error.message(),
)
}
+14 -22
View File
@@ -3,13 +3,12 @@ use std::sync::Arc;
use axum::{
body::Bytes,
extract::{Path, Query as QueryParams, State},
http::{HeaderMap, StatusCode},
response::Response,
http::HeaderMap,
};
use serde::Deserialize;
use crate::api::{Api, AuthedPubkey};
use crate::web::{ApiError, err, ok};
use crate::web::{ApiResult, bad_request, internal, ok};
#[derive(Deserialize)]
pub struct StripeSessionParams {
@@ -21,22 +20,16 @@ pub async fn create_stripe_session(
AuthedPubkey(auth): AuthedPubkey,
Path(pubkey): Path<String>,
QueryParams(params): QueryParams<StripeSessionParams>,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin_or_tenant(&auth, &pubkey)?;
let tenant = api.get_tenant_or_404(&pubkey).await?;
match api
let url = api
.billing
.stripe_create_portal_session(&tenant.stripe_customer_id, params.return_url.as_deref())
.await
{
Ok(url) => Ok(ok(StatusCode::OK, serde_json::json!({ "url": url }))),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
.map_err(internal)?;
ok(serde_json::json!({ "url": url }))
}
/// Stripe webhook endpoint. Authenticated via `Stripe-Signature` verification
@@ -45,19 +38,18 @@ pub async fn stripe_webhook(
State(api): State<Arc<Api>>,
headers: HeaderMap,
body: Bytes,
) -> Response {
) -> ApiResult {
let signature = headers
.get("Stripe-Signature")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let payload = match std::str::from_utf8(&body) {
Ok(s) => s,
Err(_) => return err(StatusCode::BAD_REQUEST, "bad-request", "invalid payload"),
};
let payload = std::str::from_utf8(&body)
.map_err(|_| bad_request("bad-request", "invalid payload"))?;
match api.billing.handle_webhook(payload, signature).await {
Ok(()) => ok(StatusCode::OK, ()),
Err(e) => err(StatusCode::BAD_REQUEST, "webhook-error", &e.to_string()),
}
api.billing
.handle_webhook(payload, signature)
.await
.map_err(|e| bad_request("webhook-error", &e.to_string()))?;
ok(())
}
+61 -107
View File
@@ -3,15 +3,13 @@ use std::sync::Arc;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::Response,
};
use serde::{Deserialize, Serialize};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use crate::api::{Api, AuthedPubkey};
use crate::models::Tenant;
use crate::web::{ApiError, err, map_unique_error, ok};
use crate::web::{ApiResult, internal, map_unique_error, ok};
#[derive(Serialize)]
pub struct TenantResponse {
@@ -46,23 +44,14 @@ pub struct UpdateTenantRequest {
pub async fn list_tenants(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin(&auth)?;
match api.query.list_tenants().await {
Ok(tenants) => Ok(ok(
StatusCode::OK,
tenants
.into_iter()
.map(TenantResponse::from)
.collect::<Vec<_>>(),
)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
let tenants = api.query.list_tenants().await.map_err(internal)?;
ok(tenants
.into_iter()
.map(TenantResponse::from)
.collect::<Vec<_>>())
}
/// Creates the tenant row for the calling pubkey. Idempotent: if the tenant
@@ -71,60 +60,37 @@ pub async fn list_tenants(
pub async fn create_tenant(
State(api): State<Arc<Api>>,
AuthedPubkey(pubkey): AuthedPubkey,
) -> Result<Response, ApiError> {
match api.query.get_tenant(&pubkey).await {
Ok(Some(t)) => Ok(ok(StatusCode::OK, TenantResponse::from(t))),
Ok(None) => {
let stripe_customer_id = match api.billing.stripe_create_customer(&pubkey).await {
Ok(id) => id,
Err(e) => {
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"stripe-customer-create-failed",
&e.to_string(),
));
}
};
) -> ApiResult {
if let Some(t) = api.query.get_tenant(&pubkey).await.map_err(internal)? {
return ok(TenantResponse::from(t));
}
let tenant = Tenant {
pubkey: pubkey.clone(),
nwc_url: String::new(),
nwc_error: None,
created_at: Utc::now().timestamp(),
stripe_customer_id,
stripe_subscription_id: None,
past_due_at: None,
};
let stripe_customer_id = api
.billing
.stripe_create_customer(&pubkey)
.await
.map_err(internal)?;
match api.command.create_tenant(&tenant).await {
Ok(()) => Ok(ok(StatusCode::OK, TenantResponse::from(tenant))),
Err(e) if matches!(map_unique_error(&e), Some("pubkey-exists")) => {
match api.query.get_tenant(&pubkey).await {
Ok(Some(t)) => Ok(ok(StatusCode::OK, TenantResponse::from(t))),
Ok(None) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
"tenant row missing after unique-constraint race",
)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
}
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
let tenant = Tenant {
pubkey: pubkey.clone(),
nwc_url: String::new(),
nwc_error: None,
created_at: Utc::now().timestamp(),
stripe_customer_id,
stripe_subscription_id: None,
past_due_at: None,
};
match api.command.create_tenant(&tenant).await {
Ok(()) => ok(TenantResponse::from(tenant)),
Err(e) if matches!(map_unique_error(&e), Some("pubkey-exists")) => {
match api.query.get_tenant(&pubkey).await {
Ok(Some(t)) => ok(TenantResponse::from(t)),
Ok(None) => Err(internal("tenant row missing after unique-constraint race")),
Err(e) => Err(internal(e)),
}
}
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
Err(e) => Err(internal(e)),
}
}
@@ -132,10 +98,10 @@ pub async fn get_tenant(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(pubkey): Path<String>,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin_or_tenant(&auth, &pubkey)?;
let tenant = api.get_tenant_or_404(&pubkey).await?;
Ok(ok(StatusCode::OK, TenantResponse::from(tenant)))
ok(TenantResponse::from(tenant))
}
pub async fn update_tenant(
@@ -143,7 +109,7 @@ pub async fn update_tenant(
AuthedPubkey(auth): AuthedPubkey,
Path(pubkey): Path<String>,
Json(payload): Json<UpdateTenantRequest>,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin_or_tenant(&auth, &pubkey)?;
let mut tenant = api.get_tenant_or_404(&pubkey).await?;
@@ -152,52 +118,40 @@ pub async fn update_tenant(
if nwc_url.is_empty() {
tenant.nwc_url = String::new();
} else {
tenant.nwc_url = api
.env
.encrypt(&nwc_url)
.map_err(|e| ApiError::Internal(e.to_string()))?;
tenant.nwc_url = api.env.encrypt(&nwc_url).map_err(internal)?;
}
}
match api.command.update_tenant(&tenant).await {
Ok(()) => {
// When NWC is first connected, attempt to pay any outstanding open invoices.
if nwc_previously_empty && !tenant.nwc_url.is_empty() {
let billing = api.billing.clone();
let tenant_clone = tenant.clone();
tokio::spawn(async move {
if let Err(e) = billing.pay_outstanding_nwc_invoices(&tenant_clone).await {
tracing::error!(
error = %e,
pubkey = %tenant_clone.pubkey,
"pay_outstanding_nwc_invoices failed after NWC setup"
);
}
});
api.command.update_tenant(&tenant).await.map_err(internal)?;
// When NWC is first connected, attempt to pay any outstanding open invoices.
if nwc_previously_empty && !tenant.nwc_url.is_empty() {
let billing = api.billing.clone();
let tenant_clone = tenant.clone();
tokio::spawn(async move {
if let Err(e) = billing.pay_outstanding_nwc_invoices(&tenant_clone).await {
tracing::error!(
error = %e,
pubkey = %tenant_clone.pubkey,
"pay_outstanding_nwc_invoices failed after NWC setup"
);
}
Ok(ok(StatusCode::OK, TenantResponse::from(tenant)))
}
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
});
}
ok(TenantResponse::from(tenant))
}
pub async fn list_tenant_relays(
State(api): State<Arc<Api>>,
AuthedPubkey(auth): AuthedPubkey,
Path(pubkey): Path<String>,
) -> Result<Response, ApiError> {
) -> ApiResult {
api.require_admin_or_tenant(&auth, &pubkey)?;
match api.query.list_relays_for_tenant(&pubkey).await {
Ok(relays) => Ok(ok(StatusCode::OK, relays)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)),
}
let relays = api
.query
.list_relays_for_tenant(&pubkey)
.await
.map_err(internal)?;
ok(relays)
}