From 49d7786e93c95fa407165f0ebe08f28fd37e7c8b Mon Sep 17 00:00:00 2001 From: userAdityaa Date: Sat, 11 Apr 2026 15:36:49 +0545 Subject: [PATCH] feat(backend): support npub and nsec formats in ADMINS env for admin auth --- backend/.env.template | 2 +- backend/README.md | 2 +- backend/src/api.rs | 67 ++++++++++++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/backend/.env.template b/backend/.env.template index a21db77..d44fc3c 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -4,7 +4,7 @@ PORT=3000 ALLOW_ORIGINS= # Optional comma-separated allowed CORS origins; empty = permissive # Auth -ADMINS= # Comma-separated hex pubkeys with admin access +ADMINS= # Comma-separated admin keys (hex pubkey or npub) # Database DATABASE_URL=sqlite://data/caravel.db diff --git a/backend/README.md b/backend/README.md index 0989c15..ce99100 100644 --- a/backend/README.md +++ b/backend/README.md @@ -35,7 +35,7 @@ Environment variables: | `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite:///data/caravel.db` | | `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` | | `PORT` | API bind port | `3000` | -| `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ | +| `ADMINS` | Comma-separated admin pubkeys (`hex` or `npub`) | _optional_ | | `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ | | `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ | | `ZOOID_API_SECRET` | Nostr secret key used for authentication of requests to the zooid API | _required_ | diff --git a/backend/src/api.rs b/backend/src/api.rs index e193ff4..67cbeca 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -9,7 +9,7 @@ use axum::{ routing::{get, post}, }; use base64::Engine; -use nostr_sdk::{Event, JsonUtil, Kind}; +use nostr_sdk::{Event, JsonUtil, Keys, Kind, PublicKey}; use serde::{Deserialize, Serialize}; use crate::billing::Billing; @@ -90,8 +90,7 @@ impl Api { let admins = std::env::var("ADMINS") .unwrap_or_default() .split(',') - .map(|v| v.trim().to_lowercase()) - .filter(|v| !v.is_empty()) + .filter_map(parse_admin_pubkey) .collect(); Self { host, @@ -122,7 +121,10 @@ impl Api { .route("/tenants/:pubkey/invoices", get(list_tenant_invoices)) .route("/invoices/:id", get(get_invoice)) .route("/invoices/:id/bolt11", get(get_invoice_bolt11)) - .route("/tenants/:pubkey/stripe/session", get(create_stripe_session)) + .route( + "/tenants/:pubkey/stripe/session", + get(create_stripe_session), + ) .route("/stripe/webhook", post(stripe_webhook)) .with_state(state) } @@ -252,6 +254,24 @@ impl Api { } } +fn parse_admin_pubkey(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + return None; + } + + if let Ok(pubkey) = PublicKey::parse(value) { + return Some(pubkey.to_hex()); + } + + // Allow nsec values by deriving their pubkey so admin matching still works. + if let Ok(keys) = Keys::parse(value) { + return Some(keys.public_key().to_hex()); + } + + None +} + fn ok(status: StatusCode, data: T) -> Response { (status, Json(OkResponse { data, code: "ok" })).into_response() } @@ -393,13 +413,7 @@ async fn get_identity( }; } - Ok(ok( - StatusCode::OK, - IdentityResponse { - pubkey, - is_admin, - }, - )) + Ok(ok(StatusCode::OK, IdentityResponse { pubkey, is_admin })) } async fn get_plan(Path(id): Path) -> Response { @@ -489,14 +503,27 @@ async fn list_relay_activity( let relay = match state.api.query.get_relay(&id).await { Ok(Some(r)) => r, Ok(None) => return Ok(err(StatusCode::NOT_FOUND, "not-found", "relay not found")), - Err(e) => return Ok(err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string())), + Err(e) => { + return Ok(err( + StatusCode::INTERNAL_SERVER_ERROR, + "internal", + &e.to_string(), + )); + } }; state.api.require_admin_or_tenant(&auth, &relay.tenant)?; match state.api.query.list_activity_for_relay(&id).await { - Ok(activity) => Ok(ok(StatusCode::OK, serde_json::json!({ "activity": activity }))), - Err(e) => Ok(err(StatusCode::INTERNAL_SERVER_ERROR, "internal", &e.to_string())), + Ok(activity) => Ok(ok( + StatusCode::OK, + serde_json::json!({ "activity": activity }), + )), + Err(e) => Ok(err( + StatusCode::INTERNAL_SERVER_ERROR, + "internal", + &e.to_string(), + )), } } @@ -773,7 +800,11 @@ async fn get_invoice( Path(id): Path, ) -> std::result::Result { let auth = state.api.extract_auth_pubkey(&headers)?; - let (invoice, tenant) = state.api.billing.get_invoice_with_tenant(&id).await + let (invoice, tenant) = state + .api + .billing + .get_invoice_with_tenant(&id) + .await .map_err(|e| ApiError::Internal(e.to_string()))?; state.api.require_admin_or_tenant(&auth, &tenant.pubkey)?; @@ -786,7 +817,11 @@ async fn get_invoice_bolt11( Path(id): Path, ) -> std::result::Result { let auth = state.api.extract_auth_pubkey(&headers)?; - let (invoice, tenant) = state.api.billing.get_invoice_with_tenant(&id).await + let (invoice, tenant) = state + .api + .billing + .get_invoice_with_tenant(&id) + .await .map_err(|e| ApiError::Internal(e.to_string()))?; state.api.require_admin_or_tenant(&auth, &tenant.pubkey)?; -- 2.52.0