feat(openapi): document bearer auth on the 28 token-gated routes

The spec carried no security scheme, so generated clients/docs could not tell
which endpoints require a session token — e.g. /wallet/utxo/balance read as
public while its handler enforces auth via extract_user_id_from_token. Add a
`bearer_auth` HTTP/JWT scheme through a Modify addon and annotate every route
that resolves a session; auth-bootstrap, public username/key lookups, and
system routes stay open. Regenerated openapi.json (drift clean).
This commit is contained in:
jwinterm
2026-06-28 21:42:32 -04:00
parent 29efbf4494
commit 56c8dd463a
8 changed files with 236 additions and 29 deletions
+2
View File
@@ -857,6 +857,7 @@ pub async fn logout(
/// The authenticated user's own info.
#[utoipa::path(
security(("bearer_auth" = [])),
get,
path = "/auth/me",
responses(
@@ -1009,6 +1010,7 @@ pub struct NostrLinkResponse {
/// shape but refuses every link, because it cannot yet prove the nonce was issued
/// by this server and is unused.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/auth/nostr/link",
request_body = NostrLinkRequest,
+30
View File
@@ -14,6 +14,7 @@ use utoipa::OpenApi;
version = "0.3.0",
description = "Open, self-hostable backend for the Smirk non-custodial multi-chain wallet."
),
modifiers(&SecurityAddon),
paths(
crate::api::health::health,
crate::api::capabilities::capabilities,
@@ -144,3 +145,32 @@ use utoipa::OpenApi;
)
)]
pub struct ApiDoc;
/// Registers the `bearer_auth` security scheme (HTTP Bearer / JWT). Routes that
/// resolve a session via `extract_user_id_from_token` annotate themselves with
/// `security(("bearer_auth" = []))`; public routes (auth bootstrap, lookups,
/// system) carry no `security` and are documented as open. Without this the
/// generated spec/client could not tell which endpoints require a token.
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
let components = openapi
.components
.as_mut()
.expect("components are registered by the derive");
components.add_security_scheme(
"bearer_auth",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.description(Some(
"Session access token issued by the /auth/* endpoints.",
))
.build(),
),
);
}
}
+3
View File
@@ -209,6 +209,7 @@ pub struct UserKeysResponse {
/// reserved list before the DB write. The UNIQUE constraint is the atomic claim:
/// a collision (taken or reserved) is a 409, never a 500.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/users/me/username",
request_body = SetUsernameRequest,
@@ -244,6 +245,7 @@ pub async fn set_username(
/// The authenticated user's own username (or `null`).
#[utoipa::path(
security(("bearer_auth" = [])),
get,
path = "/users/me/username",
responses(
@@ -327,6 +329,7 @@ pub async fn lookup_username(
/// Idempotent: the DB upsert keys on `(user_id, asset, key_type)`, so re-posting
/// the same asset replaces the stored key material in place.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/keys",
request_body = RegisterKeyRequest,
+6
View File
@@ -166,6 +166,7 @@ pub struct BroadcastResponse {
/// Confirmed/unconfirmed balance for a BTC/LTC address.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/utxo/balance",
request_body = AddressRequest,
@@ -198,6 +199,7 @@ pub async fn balance(
/// Unspent outputs for a BTC/LTC address.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/utxo/utxos",
request_body = AddressRequest,
@@ -237,6 +239,7 @@ pub async fn utxos(
/// Confirmed + mempool transaction history for a BTC/LTC address.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/utxo/history",
request_body = AddressRequest,
@@ -275,6 +278,7 @@ pub async fn history(
/// Best-chain tip height (for client-side confirmation counting).
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/utxo/tip",
request_body = AssetRequest,
@@ -300,6 +304,7 @@ pub async fn tip(
/// Fee-rate estimate (sat/vB) for confirmation within `blocks` blocks.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/utxo/fee",
request_body = FeeRequest,
@@ -327,6 +332,7 @@ pub async fn fee(
/// Broadcast a finalized, signed BTC/LTC transaction.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/utxo/broadcast",
request_body = BroadcastRequest,
+3
View File
@@ -103,6 +103,7 @@ fn output_dto(o: ViewWalletOutputResult) -> GrinOutput {
/// View-only scan for the outputs a `rewind_hash` recognizes.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/scan",
request_body = GrinScanRequest,
@@ -135,6 +136,7 @@ pub async fn scan(
/// Current Grin chain tip height (for confirmation counting).
#[utoipa::path(
security(("bearer_auth" = [])),
get,
path = "/wallet/grin/height",
responses(
@@ -157,6 +159,7 @@ pub async fn height(
/// Broadcast a finalized, signed Grin transaction.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/broadcast",
request_body = GrinBroadcastRequest,
+6
View File
@@ -201,6 +201,7 @@ fn validate_slatepack(value: &str, field: &str) -> Result<(), AppError> {
/// Post a slatepack addressed to a registered recipient (sender = caller).
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/relay/create",
request_body = CreateRelayRequest,
@@ -252,6 +253,7 @@ pub async fn create(
/// The caller's inbox: relays awaiting their response.
#[utoipa::path(
security(("bearer_auth" = [])),
get,
path = "/wallet/grin/relay/pending",
responses(
@@ -277,6 +279,7 @@ pub async fn pending(
/// Poll a relay's current state (sender or recipient). The sender uses this to
/// fetch the recipient's response once status is `pending_sender`.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/relay/get",
request_body = SlateIdRequest,
@@ -301,6 +304,7 @@ pub async fn get_relay(
/// Attach the recipient's response, advancing to `pending_sender`.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/relay/respond",
request_body = RespondRelayRequest,
@@ -339,6 +343,7 @@ pub async fn respond(
/// The backend does NOT finalize or broadcast — the wallet does that locally and
/// reports the resulting tx hash here.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/relay/finalize",
request_body = FinalizeRelayRequest,
@@ -377,6 +382,7 @@ pub async fn finalize(
/// Cancel a relay (sender or recipient), unless already finalized.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/grin/relay/cancel",
request_body = SlateIdRequest,
+8
View File
@@ -274,6 +274,7 @@ fn random_dto(r: RandomOutput) -> RandomOutputDto {
/// Register an account (its view key) with the LWS so it begins scanning.
/// Idempotent at the LWS; pass `start_height` to scan from a wallet birthday.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/register",
request_body = RegisterRequest,
@@ -310,6 +311,7 @@ pub async fn register(
/// Balance + scan state for an account.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/balance",
request_body = ViewRequest,
@@ -347,6 +349,7 @@ pub async fn balance(
/// Transaction history (confirmed + mempool) for an account.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/history",
request_body = ViewRequest,
@@ -379,6 +382,7 @@ pub async fn history(
/// Unspent outputs for an account (for client-side spend construction).
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/unspent_outs",
request_body = ViewRequest,
@@ -414,6 +418,7 @@ pub async fn unspent_outs(
/// Random decoy outputs for ring construction.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/random_outs",
request_body = RandomOutsRequest,
@@ -452,6 +457,7 @@ pub async fn random_outs(
/// Broadcast a finalized, signed Monero/Wownero transaction.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/submit_tx",
request_body = SubmitRequest,
@@ -480,6 +486,7 @@ pub async fn submit_tx(
/// Current chain height (daemon) for confirmation counting.
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/height",
request_body = LwsAssetRequest,
@@ -505,6 +512,7 @@ pub async fn height(
/// Confirmation count for a transaction (daemon).
#[utoipa::path(
security(("bearer_auth" = [])),
post,
path = "/wallet/lws/confirmations",
request_body = ConfirmationsRequest,