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`
- 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
## `async fn list_relays(...) -> Response`
- Serves `GET /relays?tenant=<pubkey>`
- Authorizes admin or existing tenants
- If user is admin, `tenant` query parameter is optional
- If user is a tenant, `tenant` query parameter is not ok; authenticated `pubkey` is used
- Serves `GET /relays`
- Authorizes admin only
- Return `data` is a list of relay structs from `repo.list_relays`
## `async fn get_relay(...) -> Response`
@@ -103,22 +120,19 @@ Notes:
- Deactivates relay using `repo.deactivate_relay`
- Return `data` is empty
--- Billing routes
--- Invoice routes
## `async fn list_invoices(...) -> Response`
- Serves `GET /invoices?tenant=<pubkey>`
- Authorizes admin or existing tenants
- If user is admin, `tenant` query parameter is optional
- If user is a tenant, `tenant` query parameter is not ok; authenticated `pubkey` is used
- Serves `GET /invoices`
- Authorizes admin only
- 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`
- Authorizes admin or matching tenant
- Updates tenant billing NWC URL using `repo.update_tenant_nwc_url`
- Return `data` is the submitted billing payload
- Serves `GET /invoices/:id`
- Authorizes admin or invoice owner
- Return `data` is a single invoice struct from `repo.get_invoice`
# Utility functions
+95 -68
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow};
use axum::{
Json, Router,
extract::{Path, Query, State},
extract::{Path, State},
http::{HeaderMap, Method, StatusCode, Uri},
response::{IntoResponse, Response},
routing::{get, post, put},
@@ -80,11 +80,14 @@ impl Api {
.route("/plans/:id", get(get_plan))
.route("/tenants", get(list_tenants).post(create_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("/relays", get(list_relays).post(create_relay))
.route("/relays/:id", get(get_relay).put(update_relay))
.route("/relays/:id/deactivate", post(deactivate_relay))
.route("/invoices", get(list_invoices))
.route("/invoices/:id", get(get_invoice))
.with_state(state)
.layer(self.cors_layer());
@@ -259,11 +262,6 @@ fn extract_auth_pubkey(
Ok(event.pubkey.to_hex())
}
#[derive(Deserialize)]
struct TenantParam {
tenant: Option<String>,
}
#[derive(Deserialize, Serialize)]
struct UpdateTenantBillingRequest {
nwc_url: String,
@@ -423,43 +421,42 @@ async fn list_relays(
headers: HeaderMap,
method: Method,
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 {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
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()
{
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())
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &pubkey)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
let relays = match tenant_filter {
Some(tenant) => state.api.repo.list_relays_for_tenant(tenant).await,
None => state.api.repo.list_relays().await,
};
match relays {
match state.api.repo.list_relays_for_tenant(&pubkey).await {
Ok(relays) => ok(StatusCode::OK, relays),
Err(e) => err(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -721,43 +718,16 @@ async fn list_invoices(
headers: HeaderMap,
method: Method,
uri: Uri,
Query(query): Query<TenantParam>,
) -> 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,
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) {
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 {
match state.api.repo.list_invoices().await {
Ok(invoices) => ok(StatusCode::OK, invoices),
Err(e) => err(
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(
State(state): State<AppState>,
headers: HeaderMap,
+13
View File
@@ -382,6 +382,19 @@ impl Repo {
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<()> {
let mut tx = self.pool.begin().await?;