From 9f737a25cd0c1921b5b151161c7317c3c14647a7 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 26 Mar 2026 13:28:06 -0700 Subject: [PATCH] refactor auth --- backend/spec/api.md | 19 +- backend/src/api.rs | 553 +++++++++++++++++++------------------------- 2 files changed, 250 insertions(+), 322 deletions(-) diff --git a/backend/spec/api.md b/backend/spec/api.md index 1fccc73..5c218e4 100644 --- a/backend/spec/api.md +++ b/backend/spec/api.md @@ -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` +## `extract_auth_pubkey(&self, headers: &HeaderMap) -> Result` - 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` +## `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` - Validate `subdomain` - If `plan` is free and `blossom` is enabled, return `premium-feature` diff --git a/backend/src/api.rs b/backend/src/api.rs index 0fb2591..24252f5 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -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 { + 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::; + 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 { + 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 { - 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::()?; 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 { - 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::; - let mut got_m = None::; - 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, 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 { + 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, 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(id): Path, -) -> Response { - if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { - return auth_fail_response(e); - } +) -> std::result::Result { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(pubkey): Path, -) -> 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 { + 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, 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 { + 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, 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(pubkey): Path, -) -> 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(id): Path, -) -> 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Json(payload): Json, -) -> 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(id): Path, Json(payload): Json, -) -> 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(id): Path, -) -> 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 { + 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, 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(pubkey): Path, -) -> 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(id): Path, -) -> 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 { + 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, headers: HeaderMap, - method: Method, - uri: Uri, Path(pubkey): Path, Json(payload): Json, -) -> 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 { + 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(), - ), + )), } }