forked from coracle/caravel
c261d8a146
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com> Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
8.6 KiB
8.6 KiB
pub struct Api
Api manages the HTTP interface for the application
Members:
host: String- the hostname of the service for checking NIP 98 auth, fromHOSTadmins: Vec<String>- a list of admin pubkeys fromADMINSquery: Querycommand: Commandbilling: Billinginfra: Infra
Notes:
- Authentication is done using NIP 98 comparing
utoself.host, not the incoming request - Each route is responsible for authorization using
self.require_adminorself.require_admin_or_tenant - Successful API responses should be of the form
{data, code: "ok"}with an appropriate http status code. - Unsuccessful API responses should be of the form
{error, code}with an appropriate http status code.codeis a short error code (e.g.duplicate-subdomain) anderroris a human-readable error message.
pub fn new() -> Self
- Reads environment and populates members
pub fn router(&self) -> Result<()>
- Returns an
axum::Router
--- Plan routes
async fn list_plans(...) -> Response
- Serves
GET /plans - No authentication required
- Return
datais a list of plan structs fromQuery::list_plans
async fn get_plan(...) -> Response
- Serves
GET /plans/:id - No authentication required
- Return
datais a single plan struct matchingid - If plan does not exist, return
404withcode=not-found
--- Identity routes
async fn get_identity(...) -> Response
- Serves
GET /identity - Authorizes anyone, but must be authorized
- Side-effect-free: returns
{ pubkey, is_admin }only - Clients must call
POST /tenantsbefore any tenant-scoped write - Return
datais anIdentitystruct
--- Tenant routes
async fn list_tenants(...) -> Response
- Serves
GET /tenants - Authorizes admin only
- Return
datais a list of tenant structs fromquery.list_tenants
async fn create_tenant(...) -> Response
- Serves
POST /tenants - Authorizes anyone, but must be authorized
- No request body; target pubkey is derived from NIP-98 auth
- Idempotent: if a tenant already exists for the auth pubkey, return it without calling Stripe or writing to the DB
- Otherwise, call the Stripe API to create a new customer and create a new tenant using
command.create_tenantwith the resultingstripe_customer_id. No subscription is created yet — that happens when the first relay is added. - On unique-constraint race (
pubkey-exists), re-fetch and return the existing tenant - If Stripe customer creation fails, return
code=stripe-customer-create-failed - Always returns
200(create-or-get is uniform) - Return
datais a singleTenantstruct
async fn get_tenant(...) -> Response
- Serves
GET /tenants/:pubkey - Authorizes admin or matching tenant
- Return
datais a single tenant struct fromquery.get_tenant
async fn update_tenant(...) -> Response
- Serves
PUT /tenants/:pubkey - Authorizes admin or matching tenant
- Updates tenant using
command.update_tenant - Return
datais the updated tenant struct
async fn list_tenant_relays(...) -> Response
- Serves
GET /tenants/:pubkey/relays - Authorizes admin or matching tenant
- Return
datais a list of relay structs fromquery.list_relays_for_tenant
--- Relay routes
async fn list_relays(...) -> Response
- Serves
GET /relays - Authorizes admin only
- Return
datais a list of relay structs fromquery.list_relays
async fn get_relay(...) -> Response
- Serves
GET /relays/:id - Authorizes admin or relay owner
- Return
datais a single relay struct fromquery.get_relay
async fn list_relay_members(...) -> Response
- Serves
GET /relays/:id/members - Authorizes admin or relay owner
- For unsynced relays, returns an empty member list without calling zooid
- For synced relays, proxies the member list from zooid via
infra - Return
datais{ members }
async fn create_relay(...) -> Response
- Serves
POST /relays - Authorizes admin or matching tenant pubkey in request body
- Validates/prepares the relay data to be saved using
prepare_relay - Creates a new relay using
command.create_relay - If relay is a duplicate by subdomain, return a
422withcode=subdomain-exists - Return
datais a single relay struct. Use HTTP201.
async fn update_relay(...) -> Response
- Serves
PUT /relays/:id - Authorizes admin or relay owner
- Validates/prepares the relay data to be saved using
prepare_relay - If the requested plan changes to a plan with a finite member limit and the current member count exceeds that limit, return a
422withcode=member-limit-exceeded - Updates the given relay using
command.update_relay - If relay is a duplicate by subdomain, return a
422withcode=subdomain-exists - Return
datais a single relay struct.
async fn list_relay_activity(...) -> Response
- Serves
GET /relays/:id/activity - Authorizes admin or relay owner
- Get activity from
query.list_activity_for_relay - Return
datais{activity}
async fn deactivate_relay(...) -> Response
- Serves
POST /relays/:id/deactivate - Authorizes admin or relay owner
- If relay status is
inactiveordelinquent, return a400withcode=relay-is-inactive - Call
command.deactivate_relay - Return
datais empty
async fn reactivate_relay(...) -> Response
- Serves
POST /relays/:id/reactivate - Authorizes admin or relay owner
- If relay is already active, return a
400withcode=relay-is-active - Call
command.activate_relay - Return
datais empty
--- Invoice routes
async fn list_tenant_invoices(...) -> Response
- Serves
GET /tenants/:pubkey/invoices - Authorizes admin or matching tenant
- Looks up tenant by pubkey, fetches invoices from Stripe API using
stripe_customer_id - Return
datais a list of Stripe invoice objects:{ id, status, amount_due, currency, hosted_invoice_url, period_start, period_end }
async fn get_invoice(...) -> Response
- Serves
GET /invoices/:id - Fetches invoice from Stripe API by ID
- Looks up tenant by the invoice's
customerfield, authorizes admin or matching tenant - Return
datais a single Stripe invoice object - If invoice does not exist, return
404withcode=not-found
async fn get_invoice_bolt11(...) -> Response
- Serves
GET /invoices/:id/bolt11 - Fetches invoice from Stripe API by ID
- Looks up tenant by the invoice's
customerfield, authorizes admin or matching tenant - If invoice
statusis notopen, return400withcode=invoice-not-open - Creates a bolt11 Lightning invoice for the invoice's
amount_dueusingbilling.create_bolt11(amount_due) - Return
datais{ bolt11 }
--- Stripe session route
async fn create_stripe_session(...) -> Response
- Serves
GET /tenants/:pubkey/stripe/session - Authorizes admin or matching tenant
- Looks up tenant by pubkey
- Creates a Stripe Customer Portal session for the tenant's
stripe_customer_id - Return
datais{ url }— the portal session URL
--- Stripe webhook route
async fn stripe_webhook(...) -> Response
- Serves
POST /stripe/webhook - No NIP-98 authentication — uses Stripe signature verification instead
- Reads raw request body and
Stripe-Signatureheader - Calls
billing.handle_webhook(payload, signature) - Returns
200on success,400on signature verification failure - Startup requires non-empty
STRIPE_WEBHOOK_SECRET
--- Utilities
extract_auth_pubkey(&self, headers: &HeaderMap) -> Result<String>
- Parses
Authorizationheader - Validates event kind (
27235) and signature usingnostr_sdk - Validates event
ucontains configuredHOST - Intentionally does not enforce exact request URL/method/query matching
- Intentionally does not validate
payloadtag/hash,created_atfreshness window, or replay nonce/cache - This is a deliberate session-style tradeoff to reduce repeated signer prompts in the client
- 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.
require_admin(&self, authorized_pubkey: &str)
- Checks whether
authorized_pubkeyis inself.admins. If not, returns an forbidden error
require_admin_or_tenant(&self, authorized_pubkey: &str, tenant_pubkey: &str)
- Checks whether
authorized_pubkeyis an admin or matchestenant_pubkey
prepare_relay(&self, relay: Relay) -> anyhow::Result<Relay>
- Validate
subdomain - Validate that
planmatches a known plan id fromQuery::list_plans - If selected
plandoes not includeblossomandblossomis enabled, returnpremium-feature - If selected
plandoes not includelivekitandlivekitis enabled, returnpremium-feature - Populate
schemaif not already set - Populate missing fields using reasonable defaults