Split api routes up

This commit is contained in:
Jon Staab
2026-03-26 12:59:29 -07:00
parent 619fd0c2ce
commit 6d651e2722
3 changed files with 136 additions and 82 deletions
+28 -14
View File
@@ -62,14 +62,31 @@ Notes:
- If tenant is a duplicate, return a `422` with `code=pubkey-exists` - If tenant is a duplicate, return a `422` with `code=pubkey-exists`
- Return `data` is a single tenant struct. Use HTTP `201`. - Return `data` is a single tenant struct. Use HTTP `201`.
## `async fn list_tenant_relays(...) -> Response`
- Serves `GET /tenants/:pubkey/relays`
- Authorizes admin or matching tenant
- Return `data` is a list of relay structs from `repo.list_relays_for_tenant`
## `async fn list_tenant_invoices(...) -> Response`
- Serves `GET /tenants/:pubkey/invoices`
- Authorizes admin or matching tenant
- Return `data` is a list of invoice structs from `repo.list_invoices_for_tenant`
## `async fn update_tenant_billing(...) -> Response`
- Serves `PUT /tenants/:pubkey/billing`
- Authorizes admin or matching tenant
- Updates tenant billing NWC URL using `repo.update_tenant_nwc_url`
- Return `data` is the submitted billing payload
--- Relay routes --- Relay routes
## `async fn list_relays(...) -> Response` ## `async fn list_relays(...) -> Response`
- Serves `GET /relays?tenant=<pubkey>` - Serves `GET /relays`
- Authorizes admin or existing tenants - Authorizes admin only
- If user is admin, `tenant` query parameter is optional
- If user is a tenant, `tenant` query parameter is not ok; authenticated `pubkey` is used
- Return `data` is a list of relay structs from `repo.list_relays` - Return `data` is a list of relay structs from `repo.list_relays`
## `async fn get_relay(...) -> Response` ## `async fn get_relay(...) -> Response`
@@ -103,22 +120,19 @@ Notes:
- Deactivates relay using `repo.deactivate_relay` - Deactivates relay using `repo.deactivate_relay`
- Return `data` is empty - Return `data` is empty
--- Billing routes --- Invoice routes
## `async fn list_invoices(...) -> Response` ## `async fn list_invoices(...) -> Response`
- Serves `GET /invoices?tenant=<pubkey>` - Serves `GET /invoices`
- Authorizes admin or existing tenants - Authorizes admin only
- If user is admin, `tenant` query parameter is optional
- If user is a tenant, `tenant` query parameter is not ok; authenticated `pubkey` is used
- Return `data` is a list of invoice structs from `repo.list_invoices` - Return `data` is a list of invoice structs from `repo.list_invoices`
## `async fn update_tenant_billing(...) -> Response` ## `async fn get_invoice(...) -> Response`
- Serves `PUT /tenants/:pubkey/billing` - Serves `GET /invoices/:id`
- Authorizes admin or matching tenant - Authorizes admin or invoice owner
- Updates tenant billing NWC URL using `repo.update_tenant_nwc_url` - Return `data` is a single invoice struct from `repo.get_invoice`
- Return `data` is the submitted billing payload
# Utility functions # Utility functions
+95 -68
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use axum::{ use axum::{
Json, Router, Json, Router,
extract::{Path, Query, State}, extract::{Path, State},
http::{HeaderMap, Method, StatusCode, Uri}, http::{HeaderMap, Method, StatusCode, Uri},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::{get, post, put}, routing::{get, post, put},
@@ -80,11 +80,14 @@ impl Api {
.route("/plans/:id", get(get_plan)) .route("/plans/:id", get(get_plan))
.route("/tenants", get(list_tenants).post(create_tenant)) .route("/tenants", get(list_tenants).post(create_tenant))
.route("/tenants/:pubkey", get(get_tenant)) .route("/tenants/:pubkey", get(get_tenant))
.route("/tenants/:pubkey/relays", get(list_tenant_relays))
.route("/tenants/:pubkey/invoices", get(list_tenant_invoices))
.route("/tenants/:pubkey/billing", put(update_tenant_billing)) .route("/tenants/:pubkey/billing", put(update_tenant_billing))
.route("/relays", get(list_relays).post(create_relay)) .route("/relays", get(list_relays).post(create_relay))
.route("/relays/:id", get(get_relay).put(update_relay)) .route("/relays/:id", get(get_relay).put(update_relay))
.route("/relays/:id/deactivate", post(deactivate_relay)) .route("/relays/:id/deactivate", post(deactivate_relay))
.route("/invoices", get(list_invoices)) .route("/invoices", get(list_invoices))
.route("/invoices/:id", get(get_invoice))
.with_state(state) .with_state(state)
.layer(self.cors_layer()); .layer(self.cors_layer());
@@ -259,11 +262,6 @@ fn extract_auth_pubkey(
Ok(event.pubkey.to_hex()) Ok(event.pubkey.to_hex())
} }
#[derive(Deserialize)]
struct TenantParam {
tenant: Option<String>,
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct UpdateTenantBillingRequest { struct UpdateTenantBillingRequest {
nwc_url: String, nwc_url: String,
@@ -423,43 +421,42 @@ async fn list_relays(
headers: HeaderMap, headers: HeaderMap,
method: Method, method: Method,
uri: Uri, uri: Uri,
Query(query): Query<TenantParam>, ) -> Response {
let pubkey = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
if !state.api.is_admin(&pubkey) {
return err(StatusCode::FORBIDDEN, "forbidden", "admin required");
}
match state.api.repo.list_relays().await {
Ok(relays) => ok(StatusCode::OK, relays),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
async fn list_tenant_relays(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Path(pubkey): Path<String>,
) -> Response { ) -> Response {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v, Ok(v) => v,
Err(e) => return auth_fail_response(e), Err(e) => return auth_fail_response(e),
}; };
let tenant_filter = if state.api.is_admin(&auth) { if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &pubkey)) {
query.tenant.as_deref() return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
} else { }
if state
.api
.repo
.get_tenant(&auth)
.await
.ok()
.flatten()
.is_none()
{
return err(StatusCode::FORBIDDEN, "forbidden", "tenant required");
}
if query.tenant.is_some() {
return err(
StatusCode::BAD_REQUEST,
"tenant-query-not-allowed",
"tenant query is not allowed for tenant users",
);
}
Some(auth.as_str())
};
let relays = match tenant_filter { match state.api.repo.list_relays_for_tenant(&pubkey).await {
Some(tenant) => state.api.repo.list_relays_for_tenant(tenant).await,
None => state.api.repo.list_relays().await,
};
match relays {
Ok(relays) => ok(StatusCode::OK, relays), Ok(relays) => ok(StatusCode::OK, relays),
Err(e) => err( Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@@ -721,43 +718,16 @@ async fn list_invoices(
headers: HeaderMap, headers: HeaderMap,
method: Method, method: Method,
uri: Uri, uri: Uri,
Query(query): Query<TenantParam>,
) -> Response { ) -> Response {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let pubkey = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v, Ok(v) => v,
Err(e) => return auth_fail_response(e), Err(e) => return auth_fail_response(e),
}; };
if !state.api.is_admin(&pubkey) {
return err(StatusCode::FORBIDDEN, "forbidden", "admin required");
}
let tenant_filter = if state.api.is_admin(&auth) { match state.api.repo.list_invoices().await {
query.tenant.as_deref()
} else {
if state
.api
.repo
.get_tenant(&auth)
.await
.ok()
.flatten()
.is_none()
{
return err(StatusCode::FORBIDDEN, "forbidden", "tenant required");
}
if query.tenant.is_some() {
return err(
StatusCode::BAD_REQUEST,
"tenant-query-not-allowed",
"tenant query is not allowed for tenant users",
);
}
Some(auth.as_str())
};
let invoices = match tenant_filter {
Some(tenant) => state.api.repo.list_invoices_for_tenant(tenant).await,
None => state.api.repo.list_invoices().await,
};
match invoices {
Ok(invoices) => ok(StatusCode::OK, invoices), Ok(invoices) => ok(StatusCode::OK, invoices),
Err(e) => err( Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@@ -767,6 +737,63 @@ async fn list_invoices(
} }
} }
async fn list_tenant_invoices(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Path(pubkey): Path<String>,
) -> Response {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &pubkey)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
match state.api.repo.list_invoices_for_tenant(&pubkey).await {
Ok(invoices) => ok(StatusCode::OK, invoices),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
}
}
async fn get_invoice(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>,
) -> Response {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
let invoice = match state.api.repo.get_invoice(&id).await {
Ok(Some(i)) => i,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "invoice not found"),
Err(e) => {
return err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &invoice.tenant)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
ok(StatusCode::OK, invoice)
}
async fn update_tenant_billing( async fn update_tenant_billing(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
+13
View File
@@ -382,6 +382,19 @@ impl Repo {
Ok(rows) Ok(rows)
} }
pub async fn get_invoice(&self, id: &str) -> Result<Option<Invoice>> {
let row = sqlx::query_as::<_, Invoice>(
"SELECT id, tenant, status, created_at, attempted_at, error, closed_at,
sent_at, paid_at, bolt11, period_start, period_end
FROM invoice
WHERE id = ?",
)
.bind(id)
.fetch_optional(&self.pool)
.await?;
Ok(row)
}
pub async fn mark_invoice_paid(&self, invoice_id: &str) -> Result<()> { pub async fn mark_invoice_paid(&self, invoice_id: &str) -> Result<()> {
let mut tx = self.pool.begin().await?; let mut tx = self.pool.begin().await?;