refactor auth

This commit is contained in:
Jon Staab
2026-03-26 13:28:06 -07:00
parent 6d651e2722
commit 9f737a25cd
2 changed files with 250 additions and 322 deletions
+14 -5
View File
@@ -134,18 +134,27 @@ Notes:
- Authorizes admin or invoice owner
- Return `data` is a single invoice struct from `repo.get_invoice`
# Utility functions
--- Utilities
## `extract_auth_pubkey(headers: &HeaderMap, method: &Method, uri: &Uri) -> Result<String>`
## `extract_auth_pubkey(&self, headers: &HeaderMap) -> Result<String>`
- Parses `Authorization` header
- Validates event kind and signature using `nostr_sdk`
- Validates event `u` and `method` tags against parameters
- Returns pubkey if header is valid
- Validates event `u` against `HOST` (not the request path. Non-standard, but correct)
- Does not validate `method` tag
- Returns pubkey if header all checks pass
Refer to https://github.com/nostr-protocol/nips/blob/master/98.md for details. Use `nostr_sdk` functionality where possible.
## `prepare_relay(relay: Relay) -> anyhow::Result<Relay>`
## `require_admin(&self, authorized_pubkey: &str)`
- Checks whether `authorized_pubkey` is in `self.admins`. If not, returns an forbidden error
## `require_admin_or_tenant(&self, authorized_pubkey: &str, tenant_pubkey: &str)`
- Checks whether `authorized_pubkey` is an admin or matches `tenant_pubkey`
## `prepare_relay(&self, relay: Relay) -> anyhow::Result<Relay>`
- Validate `subdomain`
- If `plan` is free and `blossom` is enabled, return `premium-feature`
+236 -317
View File
@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow};
use axum::{
Json, Router,
extract::{Path, State},
http::{HeaderMap, Method, StatusCode, Uri},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::{get, post, put},
};
@@ -42,6 +42,21 @@ struct ErrorResponse {
code: String,
}
#[derive(Debug)]
enum ApiError {
Unauthorized(anyhow::Error),
Forbidden(&'static str),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
Self::Unauthorized(e) => err(StatusCode::UNAUTHORIZED, "unauthorized", &e.to_string()),
Self::Forbidden(message) => err(StatusCode::FORBIDDEN, "forbidden", message),
}
}
}
impl Api {
pub fn new(repo: Repo) -> Self {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
@@ -110,12 +125,117 @@ impl Api {
}
}
fn is_admin(&self, pubkey: &str) -> bool {
self.admins.iter().any(|a| a == pubkey)
fn extract_auth_pubkey(&self, headers: &HeaderMap) -> std::result::Result<String, ApiError> {
let auth = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| anyhow!("missing authorization header"))
.map_err(ApiError::Unauthorized)?;
if !auth.starts_with("Nostr ") {
return Err(ApiError::Unauthorized(anyhow!(
"authorization must use Nostr scheme"
)));
}
let (_, b64) = auth
.split_once(' ')
.ok_or_else(|| anyhow!("malformed authorization header"))
.map_err(ApiError::Unauthorized)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(anyhow::Error::from)
.map_err(ApiError::Unauthorized)?;
let json = String::from_utf8(bytes)
.map_err(anyhow::Error::from)
.map_err(ApiError::Unauthorized)?;
let event = Event::from_json(json)
.map_err(anyhow::Error::from)
.map_err(ApiError::Unauthorized)?;
if event.kind != Kind::HttpAuth {
return Err(ApiError::Unauthorized(anyhow!("invalid nip98 kind")));
}
event
.verify()
.map_err(anyhow::Error::from)
.map_err(ApiError::Unauthorized)?;
let mut got_u = None::<String>;
for tag in &event.tags {
let values = tag.as_slice();
if values.len() >= 2 && values[0] == "u" {
got_u = Some(values[1].to_string());
}
}
let Some(got_u) = got_u else {
return Err(ApiError::Unauthorized(anyhow!("missing u tag")));
};
if !self.host.is_empty() && !got_u.contains(&self.host) {
return Err(ApiError::Unauthorized(anyhow!("authorization host mismatch")));
}
Ok(event.pubkey.to_hex())
}
fn is_tenant(&self, authorized_pubkey: &str, tenant_pubkey: &str) -> bool {
authorized_pubkey == tenant_pubkey
fn require_admin(&self, authorized_pubkey: &str) -> std::result::Result<(), ApiError> {
if self.admins.iter().any(|a| a == authorized_pubkey) {
Ok(())
} else {
Err(ApiError::Forbidden("admin required"))
}
}
fn require_admin_or_tenant(
&self,
authorized_pubkey: &str,
tenant_pubkey: &str,
) -> std::result::Result<(), ApiError> {
if self.admins.iter().any(|a| a == authorized_pubkey) || authorized_pubkey == tenant_pubkey {
Ok(())
} else {
Err(ApiError::Forbidden("not authorized"))
}
}
fn prepare_relay(&self, mut relay: Relay) -> anyhow::Result<Relay> {
if !relay
.subdomain
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(anyhow!("invalid-subdomain"));
}
if relay.plan == "free" && relay.blossom_enabled == 1 {
return Err(anyhow!("premium-feature"));
}
if relay.plan == "free" && relay.livekit_enabled == 1 {
return Err(anyhow!("premium-feature"));
}
if relay.schema.is_empty() {
relay.schema = format!("{}_{}", relay.subdomain.replace('-', "_"), relay.id);
}
if relay.status.is_empty() {
relay.status = "new".to_string();
}
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.groups_enabled = parse_bool_default(relay.groups_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.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);
Ok(relay)
}
}
@@ -146,45 +266,6 @@ fn parse_bool_default(value: i64, default: i64) -> i64 {
}
}
fn prepare_relay(mut relay: Relay) -> anyhow::Result<Relay> {
if !relay
.subdomain
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(anyhow!("invalid-subdomain"));
}
if relay.plan == "free" && relay.blossom_enabled == 1 {
return Err(anyhow!("premium-feature"));
}
if relay.plan == "free" && relay.livekit_enabled == 1 {
return Err(anyhow!("premium-feature"));
}
if relay.schema.is_empty() {
relay.schema = format!("{}_{}", relay.subdomain.replace('-', "_"), relay.id);
}
if relay.status.is_empty() {
relay.status = "new".to_string();
}
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.groups_enabled = parse_bool_default(relay.groups_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.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);
Ok(relay)
}
fn map_unique_error(err: &anyhow::Error) -> Option<&'static str> {
let sqlx_err = err.downcast_ref::<sqlx::Error>()?;
let sqlx::Error::Database(db_err) = sqlx_err else {
@@ -199,69 +280,6 @@ fn map_unique_error(err: &anyhow::Error) -> Option<&'static str> {
None
}
fn auth_fail_response(e: anyhow::Error) -> Response {
err(StatusCode::UNAUTHORIZED, "unauthorized", &e.to_string())
}
fn extract_auth_pubkey(
headers: &HeaderMap,
method: &Method,
_uri: &Uri,
host: &str,
) -> Result<String> {
let auth = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| anyhow!("missing authorization header"))?;
if !auth.starts_with("Nostr ") {
return Err(anyhow!("authorization must use Nostr scheme"));
}
let (_, b64) = auth
.split_once(' ')
.ok_or_else(|| anyhow!("malformed authorization header"))?;
let bytes = base64::engine::general_purpose::STANDARD.decode(b64)?;
let json = String::from_utf8(bytes)?;
let event = Event::from_json(json)?;
if event.kind != Kind::HttpAuth {
return Err(anyhow!("invalid nip98 kind"));
}
event.verify()?;
let expected_host = host;
let want_m = method.as_str();
let mut got_u = None::<String>;
let mut got_m = None::<String>;
for tag in event.tags.iter() {
let values = tag.as_slice();
if values.len() >= 2 {
if values[0] == "u" {
got_u = Some(values[1].to_string());
} else if values[0] == "method" {
got_m = Some(values[1].to_string());
}
}
}
let Some(got_u) = got_u else {
return Err(anyhow!("missing u tag"));
};
let Some(got_m) = got_m else {
return Err(anyhow!("missing method tag"));
};
if !expected_host.is_empty() && !got_u.contains(expected_host) {
return Err(anyhow!("authorization host mismatch"));
}
if got_m != want_m {
return Err(anyhow!("authorization method mismatch"));
}
Ok(event.pubkey.to_hex())
}
#[derive(Deserialize, Serialize)]
struct UpdateTenantBillingRequest {
nwc_url: String,
@@ -303,91 +321,66 @@ struct UpdateRelayRequest {
async fn list_tenants(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
) -> 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");
}
) -> std::result::Result<Response, ApiError> {
let pubkey = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin(&pubkey)?;
match state.api.repo.list_tenants().await {
Ok(tenants) => ok(StatusCode::OK, tenants),
Err(e) => err(
Ok(tenants) => Ok(ok(StatusCode::OK, tenants)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
)),
}
}
async fn list_plans(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
) -> Response {
if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
return auth_fail_response(e);
}
) -> std::result::Result<Response, ApiError> {
let _ = state.api.extract_auth_pubkey(&headers)?;
ok(StatusCode::OK, Repo::list_plans())
Ok(ok(StatusCode::OK, Repo::list_plans()))
}
async fn get_plan(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>,
) -> Response {
if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
return auth_fail_response(e);
}
) -> std::result::Result<Response, ApiError> {
let _ = state.api.extract_auth_pubkey(&headers)?;
match Repo::list_plans().into_iter().find(|p| p.id == id) {
Some(plan) => ok(StatusCode::OK, plan),
None => err(StatusCode::NOT_FOUND, "not-found", "plan not found"),
Some(plan) => Ok(ok(StatusCode::OK, plan)),
None => Ok(err(StatusCode::NOT_FOUND, "not-found", "plan not found")),
}
}
async fn get_tenant(
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");
}
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin_or_tenant(&auth, &pubkey)?;
match state.api.repo.get_tenant(&pubkey).await {
Ok(Some(tenant)) => ok(StatusCode::OK, tenant),
Ok(None) => err(StatusCode::NOT_FOUND, "not-found", "tenant not found"),
Err(e) => err(
Ok(Some(tenant)) => Ok(ok(StatusCode::OK, tenant)),
Ok(None) => Ok(err(StatusCode::NOT_FOUND, "not-found", "tenant not found")),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
)),
}
}
async fn create_tenant(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
) -> Response {
let pubkey = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
) -> std::result::Result<Response, ApiError> {
let pubkey = state.api.extract_auth_pubkey(&headers)?;
let tenant = Tenant {
pubkey: pubkey.clone(),
@@ -397,20 +390,20 @@ async fn create_tenant(
};
match state.api.repo.create_tenant(&tenant).await {
Ok(()) => ok(StatusCode::CREATED, tenant),
Ok(()) => Ok(ok(StatusCode::CREATED, tenant)),
Err(e) => {
if matches!(map_unique_error(&e), Some("pubkey-exists")) {
err(
Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"pubkey-exists",
"tenant already exists",
)
))
} else {
err(
Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)
))
}
}
}
@@ -419,99 +412,69 @@ async fn create_tenant(
async fn list_relays(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
) -> 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");
}
) -> std::result::Result<Response, ApiError> {
let pubkey = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin(&pubkey)?;
match state.api.repo.list_relays().await {
Ok(relays) => ok(StatusCode::OK, relays),
Err(e) => err(
Ok(relays) => Ok(ok(StatusCode::OK, relays)),
Err(e) => Ok(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),
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &pubkey)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin_or_tenant(&auth, &pubkey)?;
match state.api.repo.list_relays_for_tenant(&pubkey).await {
Ok(relays) => ok(StatusCode::OK, relays),
Err(e) => err(
Ok(relays) => Ok(ok(StatusCode::OK, relays)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
)),
}
}
async fn get_relay(
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),
};
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
let relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return err(
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
));
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
state.api.require_admin_or_tenant(&auth, &relay.tenant)?;
ok(StatusCode::OK, relay)
Ok(ok(StatusCode::OK, relay))
}
async fn create_relay(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Json(payload): Json<CreateRelayRequest>,
) -> 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, &payload.tenant)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin_or_tenant(&auth, &payload.tenant)?;
let mut relay = Relay {
id: uuid::Uuid::new_v4().to_string(),
@@ -533,39 +496,39 @@ async fn create_relay(
push_enabled: payload.push_enabled.unwrap_or(1),
};
relay = match prepare_relay(relay) {
relay = match state.api.prepare_relay(relay) {
Ok(r) => r,
Err(e) if e.to_string() == "premium-feature" => {
return err(
return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"premium-feature",
"feature requires a paid plan",
);
));
}
Err(_) => {
return err(
return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid-relay",
"relay validation failed",
);
));
}
};
match state.api.repo.create_relay(&relay).await {
Ok(()) => ok(StatusCode::CREATED, relay),
Ok(()) => Ok(ok(StatusCode::CREATED, relay)),
Err(e) => {
if matches!(map_unique_error(&e), Some("subdomain-exists")) {
err(
Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"subdomain-exists",
"subdomain already exists",
)
))
} else {
err(
Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)
))
}
}
}
@@ -574,31 +537,24 @@ async fn create_relay(
async fn update_relay(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>,
Json(payload): Json<UpdateRelayRequest>,
) -> Response {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
let mut relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return err(
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
));
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
state.api.require_admin_or_tenant(&auth, &relay.tenant)?;
if let Some(v) = payload.subdomain {
relay.subdomain = v;
@@ -637,39 +593,39 @@ async fn update_relay(
relay.push_enabled = v;
}
relay = match prepare_relay(relay) {
relay = match state.api.prepare_relay(relay) {
Ok(r) => r,
Err(e) if e.to_string() == "premium-feature" => {
return err(
return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"premium-feature",
"feature requires a paid plan",
);
));
}
Err(_) => {
return err(
return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid-relay",
"relay validation failed",
);
));
}
};
match state.api.repo.update_relay(&relay).await {
Ok(()) => ok(StatusCode::OK, relay),
Ok(()) => Ok(ok(StatusCode::OK, relay)),
Err(e) => {
if matches!(map_unique_error(&e), Some("subdomain-exists")) {
err(
Ok(err(
StatusCode::UNPROCESSABLE_ENTITY,
"subdomain-exists",
"subdomain already exists",
)
))
} else {
err(
Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
)
))
}
}
}
@@ -678,138 +634,101 @@ async fn update_relay(
async fn deactivate_relay(
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),
};
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
let relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r,
Ok(None) => return err(StatusCode::NOT_FOUND, "not-found", "relay not found"),
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")),
Err(e) => {
return err(
return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
);
));
}
};
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) {
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
state.api.require_admin_or_tenant(&auth, &relay.tenant)?;
match state.api.repo.deactivate_relay(&relay).await {
Ok(()) => ok(StatusCode::OK, ()),
Err(e) => err(
Ok(()) => Ok(ok(StatusCode::OK, ())),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
)),
}
}
async fn list_invoices(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
) -> 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");
}
) -> std::result::Result<Response, ApiError> {
let pubkey = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin(&pubkey)?;
match state.api.repo.list_invoices().await {
Ok(invoices) => ok(StatusCode::OK, invoices),
Err(e) => err(
Ok(invoices) => Ok(ok(StatusCode::OK, invoices)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
)),
}
}
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");
}
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin_or_tenant(&auth, &pubkey)?;
match state.api.repo.list_invoices_for_tenant(&pubkey).await {
Ok(invoices) => ok(StatusCode::OK, invoices),
Err(e) => err(
Ok(invoices) => Ok(ok(StatusCode::OK, invoices)),
Err(e) => Ok(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),
};
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
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"),
Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "invoice not found")),
Err(e) => {
return err(
return Ok(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");
}
state.api.require_admin_or_tenant(&auth, &invoice.tenant)?;
ok(StatusCode::OK, invoice)
Ok(ok(StatusCode::OK, invoice))
}
async fn update_tenant_billing(
State(state): State<AppState>,
headers: HeaderMap,
method: Method,
uri: Uri,
Path(pubkey): Path<String>,
Json(payload): Json<UpdateTenantBillingRequest>,
) -> 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");
}
) -> std::result::Result<Response, ApiError> {
let auth = state.api.extract_auth_pubkey(&headers)?;
state.api.require_admin_or_tenant(&auth, &pubkey)?;
match state
.api
@@ -817,11 +736,11 @@ async fn update_tenant_billing(
.update_tenant_nwc_url(&pubkey, &payload.nwc_url)
.await
{
Ok(()) => ok(StatusCode::OK, payload),
Err(e) => err(
Ok(()) => Ok(ok(StatusCode::OK, payload)),
Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR,
"internal",
&e.to_string(),
),
)),
}
}