Split api routes up
This commit is contained in:
+28
-14
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user