forked from coracle/caravel
refactor auth
This commit is contained in:
+14
-5
@@ -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
@@ -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(),
|
||||||
),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user