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