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 - Authorizes admin or invoice owner
- Return `data` is a single invoice struct from `repo.get_invoice` - 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 - Parses `Authorization` header
- Validates event kind and signature using `nostr_sdk` - Validates event kind and signature using `nostr_sdk`
- Validates event `u` and `method` tags against parameters - Validates event `u` against `HOST` (not the request path. Non-standard, but correct)
- Returns pubkey if header is valid - 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. 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` - Validate `subdomain`
- If `plan` is free and `blossom` is enabled, return `premium-feature` - 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::{ use axum::{
Json, Router, Json, Router,
extract::{Path, State}, extract::{Path, State},
http::{HeaderMap, Method, StatusCode, Uri}, http::{HeaderMap, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::{get, post, put}, routing::{get, post, put},
}; };
@@ -42,6 +42,21 @@ struct ErrorResponse {
code: String, 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 { impl Api {
pub fn new(repo: Repo) -> Self { pub fn new(repo: Repo) -> Self {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 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 { fn extract_auth_pubkey(&self, headers: &HeaderMap) -> std::result::Result<String, ApiError> {
self.admins.iter().any(|a| a == pubkey) 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 { fn require_admin(&self, authorized_pubkey: &str) -> std::result::Result<(), ApiError> {
authorized_pubkey == tenant_pubkey 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> { fn map_unique_error(err: &anyhow::Error) -> Option<&'static str> {
let sqlx_err = err.downcast_ref::<sqlx::Error>()?; let sqlx_err = err.downcast_ref::<sqlx::Error>()?;
let sqlx::Error::Database(db_err) = sqlx_err else { let sqlx::Error::Database(db_err) = sqlx_err else {
@@ -199,69 +280,6 @@ fn map_unique_error(err: &anyhow::Error) -> Option<&'static str> {
None 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)] #[derive(Deserialize, Serialize)]
struct UpdateTenantBillingRequest { struct UpdateTenantBillingRequest {
nwc_url: String, nwc_url: String,
@@ -303,91 +321,66 @@ struct UpdateRelayRequest {
async fn list_tenants( async fn list_tenants(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method, ) -> std::result::Result<Response, ApiError> {
uri: Uri, let pubkey = state.api.extract_auth_pubkey(&headers)?;
) -> Response { state.api.require_admin(&pubkey)?;
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_tenants().await { match state.api.repo.list_tenants().await {
Ok(tenants) => ok(StatusCode::OK, tenants), Ok(tenants) => Ok(ok(StatusCode::OK, tenants)),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn list_plans( async fn list_plans(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method, ) -> std::result::Result<Response, ApiError> {
uri: Uri, let _ = state.api.extract_auth_pubkey(&headers)?;
) -> Response {
if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
return auth_fail_response(e);
}
ok(StatusCode::OK, Repo::list_plans()) Ok(ok(StatusCode::OK, Repo::list_plans()))
} }
async fn get_plan( async fn get_plan(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>, Path(id): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
if let Err(e) = extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let _ = state.api.extract_auth_pubkey(&headers)?;
return auth_fail_response(e);
}
match Repo::list_plans().into_iter().find(|p| p.id == id) { match Repo::list_plans().into_iter().find(|p| p.id == id) {
Some(plan) => ok(StatusCode::OK, plan), Some(plan) => Ok(ok(StatusCode::OK, plan)),
None => err(StatusCode::NOT_FOUND, "not-found", "plan not found"), None => Ok(err(StatusCode::NOT_FOUND, "not-found", "plan not found")),
} }
} }
async fn get_tenant( async fn get_tenant(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(pubkey): Path<String>, Path(pubkey): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v, state.api.require_admin_or_tenant(&auth, &pubkey)?;
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.get_tenant(&pubkey).await { match state.api.repo.get_tenant(&pubkey).await {
Ok(Some(tenant)) => ok(StatusCode::OK, tenant), Ok(Some(tenant)) => Ok(ok(StatusCode::OK, tenant)),
Ok(None) => err(StatusCode::NOT_FOUND, "not-found", "tenant not found"), Ok(None) => Ok(err(StatusCode::NOT_FOUND, "not-found", "tenant not found")),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn create_tenant( async fn create_tenant(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method, ) -> std::result::Result<Response, ApiError> {
uri: Uri, let pubkey = state.api.extract_auth_pubkey(&headers)?;
) -> Response {
let pubkey = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) {
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
let tenant = Tenant { let tenant = Tenant {
pubkey: pubkey.clone(), pubkey: pubkey.clone(),
@@ -397,20 +390,20 @@ async fn create_tenant(
}; };
match state.api.repo.create_tenant(&tenant).await { match state.api.repo.create_tenant(&tenant).await {
Ok(()) => ok(StatusCode::CREATED, tenant), Ok(()) => Ok(ok(StatusCode::CREATED, tenant)),
Err(e) => { Err(e) => {
if matches!(map_unique_error(&e), Some("pubkey-exists")) { if matches!(map_unique_error(&e), Some("pubkey-exists")) {
err( Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"pubkey-exists", "pubkey-exists",
"tenant already exists", "tenant already exists",
) ))
} else { } else {
err( Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
) ))
} }
} }
} }
@@ -419,99 +412,69 @@ async fn create_tenant(
async fn list_relays( async fn list_relays(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method, ) -> std::result::Result<Response, ApiError> {
uri: Uri, let pubkey = state.api.extract_auth_pubkey(&headers)?;
) -> Response { state.api.require_admin(&pubkey)?;
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 { match state.api.repo.list_relays().await {
Ok(relays) => ok(StatusCode::OK, relays), Ok(relays) => Ok(ok(StatusCode::OK, relays)),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn list_tenant_relays( async fn list_tenant_relays(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(pubkey): Path<String>, Path(pubkey): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v, state.api.require_admin_or_tenant(&auth, &pubkey)?;
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_relays_for_tenant(&pubkey).await { match state.api.repo.list_relays_for_tenant(&pubkey).await {
Ok(relays) => ok(StatusCode::OK, relays), Ok(relays) => Ok(ok(StatusCode::OK, relays)),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn get_relay( async fn get_relay(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>, Path(id): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
let relay = match state.api.repo.get_relay(&id).await { let relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r, 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) => { Err(e) => {
return err( return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
); ));
} }
}; };
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) { state.api.require_admin_or_tenant(&auth, &relay.tenant)?;
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
ok(StatusCode::OK, relay) Ok(ok(StatusCode::OK, relay))
} }
async fn create_relay( async fn create_relay(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Json(payload): Json<CreateRelayRequest>, Json(payload): Json<CreateRelayRequest>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v, state.api.require_admin_or_tenant(&auth, &payload.tenant)?;
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");
}
let mut relay = Relay { let mut relay = Relay {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
@@ -533,39 +496,39 @@ async fn create_relay(
push_enabled: payload.push_enabled.unwrap_or(1), push_enabled: payload.push_enabled.unwrap_or(1),
}; };
relay = match prepare_relay(relay) { relay = match state.api.prepare_relay(relay) {
Ok(r) => r, Ok(r) => r,
Err(e) if e.to_string() == "premium-feature" => { Err(e) if e.to_string() == "premium-feature" => {
return err( return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"premium-feature", "premium-feature",
"feature requires a paid plan", "feature requires a paid plan",
); ));
} }
Err(_) => { Err(_) => {
return err( return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"invalid-relay", "invalid-relay",
"relay validation failed", "relay validation failed",
); ));
} }
}; };
match state.api.repo.create_relay(&relay).await { match state.api.repo.create_relay(&relay).await {
Ok(()) => ok(StatusCode::CREATED, relay), Ok(()) => Ok(ok(StatusCode::CREATED, relay)),
Err(e) => { Err(e) => {
if matches!(map_unique_error(&e), Some("subdomain-exists")) { if matches!(map_unique_error(&e), Some("subdomain-exists")) {
err( Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"subdomain-exists", "subdomain-exists",
"subdomain already exists", "subdomain already exists",
) ))
} else { } else {
err( Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
) ))
} }
} }
} }
@@ -574,31 +537,24 @@ async fn create_relay(
async fn update_relay( async fn update_relay(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>, Path(id): Path<String>,
Json(payload): Json<UpdateRelayRequest>, Json(payload): Json<UpdateRelayRequest>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
let mut relay = match state.api.repo.get_relay(&id).await { let mut relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r, 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) => { Err(e) => {
return err( return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
); ));
} }
}; };
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) { state.api.require_admin_or_tenant(&auth, &relay.tenant)?;
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
if let Some(v) = payload.subdomain { if let Some(v) = payload.subdomain {
relay.subdomain = v; relay.subdomain = v;
@@ -637,39 +593,39 @@ async fn update_relay(
relay.push_enabled = v; relay.push_enabled = v;
} }
relay = match prepare_relay(relay) { relay = match state.api.prepare_relay(relay) {
Ok(r) => r, Ok(r) => r,
Err(e) if e.to_string() == "premium-feature" => { Err(e) if e.to_string() == "premium-feature" => {
return err( return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"premium-feature", "premium-feature",
"feature requires a paid plan", "feature requires a paid plan",
); ));
} }
Err(_) => { Err(_) => {
return err( return Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"invalid-relay", "invalid-relay",
"relay validation failed", "relay validation failed",
); ));
} }
}; };
match state.api.repo.update_relay(&relay).await { match state.api.repo.update_relay(&relay).await {
Ok(()) => ok(StatusCode::OK, relay), Ok(()) => Ok(ok(StatusCode::OK, relay)),
Err(e) => { Err(e) => {
if matches!(map_unique_error(&e), Some("subdomain-exists")) { if matches!(map_unique_error(&e), Some("subdomain-exists")) {
err( Ok(err(
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
"subdomain-exists", "subdomain-exists",
"subdomain already exists", "subdomain already exists",
) ))
} else { } else {
err( Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
) ))
} }
} }
} }
@@ -678,138 +634,101 @@ async fn update_relay(
async fn deactivate_relay( async fn deactivate_relay(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>, Path(id): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
let relay = match state.api.repo.get_relay(&id).await { let relay = match state.api.repo.get_relay(&id).await {
Ok(Some(r)) => r, 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) => { Err(e) => {
return err( return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
); ));
} }
}; };
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &relay.tenant)) { state.api.require_admin_or_tenant(&auth, &relay.tenant)?;
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
match state.api.repo.deactivate_relay(&relay).await { match state.api.repo.deactivate_relay(&relay).await {
Ok(()) => ok(StatusCode::OK, ()), Ok(()) => Ok(ok(StatusCode::OK, ())),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn list_invoices( async fn list_invoices(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method, ) -> std::result::Result<Response, ApiError> {
uri: Uri, let pubkey = state.api.extract_auth_pubkey(&headers)?;
) -> Response { state.api.require_admin(&pubkey)?;
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_invoices().await { match state.api.repo.list_invoices().await {
Ok(invoices) => ok(StatusCode::OK, invoices), Ok(invoices) => Ok(ok(StatusCode::OK, invoices)),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn list_tenant_invoices( async fn list_tenant_invoices(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(pubkey): Path<String>, Path(pubkey): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v, state.api.require_admin_or_tenant(&auth, &pubkey)?;
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 { match state.api.repo.list_invoices_for_tenant(&pubkey).await {
Ok(invoices) => ok(StatusCode::OK, invoices), Ok(invoices) => Ok(ok(StatusCode::OK, invoices)),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }
async fn get_invoice( async fn get_invoice(
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap, headers: HeaderMap,
method: Method,
uri: Uri,
Path(id): Path<String>, Path(id): Path<String>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v,
Err(e) => return auth_fail_response(e),
};
let invoice = match state.api.repo.get_invoice(&id).await { let invoice = match state.api.repo.get_invoice(&id).await {
Ok(Some(i)) => i, 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) => { Err(e) => {
return err( return Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
); ));
} }
}; };
if !(state.api.is_admin(&auth) || state.api.is_tenant(&auth, &invoice.tenant)) { state.api.require_admin_or_tenant(&auth, &invoice.tenant)?;
return err(StatusCode::FORBIDDEN, "forbidden", "not authorized");
}
ok(StatusCode::OK, invoice) Ok(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,
method: Method,
uri: Uri,
Path(pubkey): Path<String>, Path(pubkey): Path<String>,
Json(payload): Json<UpdateTenantBillingRequest>, Json(payload): Json<UpdateTenantBillingRequest>,
) -> Response { ) -> std::result::Result<Response, ApiError> {
let auth = match extract_auth_pubkey(&headers, &method, &uri, &state.api.host) { let auth = state.api.extract_auth_pubkey(&headers)?;
Ok(v) => v, state.api.require_admin_or_tenant(&auth, &pubkey)?;
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 match state
.api .api
@@ -817,11 +736,11 @@ async fn update_tenant_billing(
.update_tenant_nwc_url(&pubkey, &payload.nwc_url) .update_tenant_nwc_url(&pubkey, &payload.nwc_url)
.await .await
{ {
Ok(()) => ok(StatusCode::OK, payload), Ok(()) => Ok(ok(StatusCode::OK, payload)),
Err(e) => err( Err(e) => Ok(err(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"internal", "internal",
&e.to_string(), &e.to_string(),
), )),
} }
} }