• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

0xmichalis / nftbk / 18659805528

20 Oct 2025 05:25PM UTC coverage: 37.563% (+1.5%) from 36.045%
18659805528

push

github

0xmichalis
chore: more log cleanup

0 of 2 new or added lines in 1 file covered. (0.0%)

129 existing lines in 4 files now uncovered.

1714 of 4563 relevant lines covered (37.56%)

7.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/server/router.rs
1
use axum::http::{header, StatusCode};
2
use axum::middleware;
3
use axum::middleware::Next;
4
use axum::{
5
    extract::Request,
6
    response::IntoResponse,
7
    routing::{delete, get, post},
8
    Router,
9
};
10
use subtle::ConstantTimeEq;
11
use tracing::{debug, warn};
12
use utoipa::OpenApi;
13
use utoipa_swagger_ui::SwaggerUi;
14

15
use crate::envvar::is_defined;
16
use crate::server::api::{
17
    ApiProblem, BackupCreateResponse, BackupRequest, BackupResponse, ProblemJson, Tokens,
18
};
19
use crate::server::auth::jwt::verify_jwt;
20
use crate::server::auth::x402::X402Config;
21
use crate::server::database::{PinInfo, TokenWithPins};
22
use crate::server::handlers::handle_archive_download::{DownloadQuery, DownloadTokenResponse};
23
use crate::server::handlers::handle_archive_download::{
24
    __path_handle_archive_download as __path_handle_download,
25
    __path_handle_archive_download_token as __path_handle_download_token,
26
    handle_archive_download as handle_download,
27
    handle_archive_download_token as handle_download_token,
28
};
29
use crate::server::handlers::handle_backup::{__path_handle_backup, handle_backup};
30
use crate::server::handlers::handle_backup_create::{
31
    __path_handle_backup_create, handle_backup_create,
32
};
33
use crate::server::handlers::handle_backup_delete_archive::{
34
    __path_handle_backup_delete_archive, handle_backup_delete_archive,
35
};
36
use crate::server::handlers::handle_backup_delete_pins::{
37
    __path_handle_backup_delete_pins, handle_backup_delete_pins,
38
};
39
use crate::server::handlers::handle_backup_retries::{
40
    __path_handle_backup_retries, handle_backup_retries,
41
};
42
use crate::server::handlers::handle_backups::BackupsQuery;
43
use crate::server::handlers::handle_backups::{__path_handle_backups, handle_backups};
44
use crate::server::handlers::handle_pins::{__path_handle_pins, handle_pins, PinsResponse};
45
use crate::server::AppState;
46

47
#[derive(OpenApi)]
48
#[openapi(
49
    paths(
50
        handle_backup_create,
51
        handle_backup,
52
        handle_download_token,
53
        handle_download,
54
        handle_backup_retries,
55
        handle_backup_delete_archive,
56
        handle_backup_delete_pins,
57
        handle_backups,
58
        handle_pins,
59
    ),
60
    components(
61
        schemas(BackupRequest, BackupCreateResponse, BackupResponse, Tokens, DownloadQuery, DownloadTokenResponse, BackupsQuery, PinsResponse, TokenWithPins, PinInfo, ApiProblem)
62
    ),
63
    tags(
64
        (name = "backups", description = "General backup operations"),
65
        (name = "pins", description = "IPFS pinning operations")
66
    ),
67
    info(
68
        title = "NFT Protection API",
69
        version = env!("CARGO_PKG_VERSION"),
70
        description = "API for protecting NFT metadata and content from EVM and Tezos NFT contracts.
71

72
## Key APIs:
73

74
### Backup Management (`/backups`)
75
- **Purpose**: Enable users to request NFT backups and either download them to their local filesystem or pin them to IPFS
76
- **Use Case**: Request a backup, track backup progress, view backup history, monitor failures
77

78
### Pin Management (`/pins`)
79
- **Purpose**: Enable users to manage IPFS pins
80
- **Use Case**: Check pin status and verify content is pinned",
81
        contact(
82
            name = "nftbk",
83
            url = "https://github.com/0xmichalis/nftbk"
84
        ),
85
        license(
86
            name = "Apache License 2.0",
87
            identifier = "Apache-2.0"
88
        )
89
    ),
90
    security(
91
        ("bearer_auth" = [])
92
    ),
93
    modifiers(&SecurityAddon)
94
)]
95
struct ApiDoc;
96

97
struct SecurityAddon;
98

99
impl utoipa::Modify for SecurityAddon {
100
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
×
101
        if let Some(components) = openapi.components.as_mut() {
×
UNCOV
102
            components.add_security_scheme(
×
103
                "bearer_auth",
104
                utoipa::openapi::security::SecurityScheme::Http(
×
105
                    utoipa::openapi::security::HttpBuilder::new()
×
106
                        .scheme(utoipa::openapi::security::HttpAuthScheme::Bearer)
×
107
                        .bearer_format("JWT")
×
108
                        .description(Some("Bearer token authentication (JWT or symmetric token)"))
×
UNCOV
109
                        .build(),
×
110
                ),
111
            );
112
        }
113
    }
114
}
115

116
#[derive(Clone)]
117
pub struct AuthState {
118
    pub app_state: AppState,
119
    pub jwt_credentials: Vec<(String, String, String)>, // (issuer, audience, verification_key)
120
}
121

UNCOV
122
async fn auth_middleware(
×
123
    axum::extract::State(auth_state): axum::extract::State<AuthState>,
124
    mut req: Request,
125
    next: Next,
126
) -> impl IntoResponse {
127
    let state = &auth_state.app_state;
×
UNCOV
128
    let jwt_credentials = &auth_state.jwt_credentials;
×
129

130
    // 1. Try symmetric token auth
131
    if let Some(ref token) = state.auth_token {
×
UNCOV
132
        let auth_header = req
×
133
            .headers()
134
            .get(header::AUTHORIZATION)
×
135
            .and_then(|v| v.to_str().ok());
×
136
        let expected = format!("Bearer {token}");
×
137
        if let Some(auth_header) = auth_header {
×
138
            if auth_header
×
139
                .as_bytes()
×
140
                .ct_eq(expected.as_bytes())
×
141
                .unwrap_u8()
×
UNCOV
142
                == 1
×
143
            {
144
                req.extensions_mut().insert(Some("admin".to_string()));
×
UNCOV
145
                return next.run(req).await;
×
146
            }
147
        }
148
    }
149

150
    // 2. Try JWT auth (multiple credential sets)
151
    if !jwt_credentials.is_empty() {
×
UNCOV
152
        let auth_header = req
×
153
            .headers()
154
            .get(header::AUTHORIZATION)
×
155
            .and_then(|v| v.to_str().ok());
×
156
        if let Some(header_value) = auth_header {
×
157
            if let Some(jwt) = header_value.strip_prefix("Bearer ") {
×
158
                for (issuer, audience, verification_key) in jwt_credentials.iter() {
×
159
                    match verify_jwt(jwt, verification_key, issuer, audience).await {
×
160
                        Ok(claims) => {
×
161
                            req.extensions_mut().insert(Some(claims.sub.clone()));
×
UNCOV
162
                            return next.run(req).await;
×
163
                        }
164
                        Err(e) => {
×
UNCOV
165
                            debug!("JWT verification failed for issuer {issuer}: {e}");
×
166
                        }
167
                    }
168
                }
UNCOV
169
                warn!("JWT verification failed for all configured credentials");
×
170
            }
171
        }
172
    }
173

174
    // 3. If both fail, return 401
175
    (
176
        [(header::WWW_AUTHENTICATE, "Bearer")],
×
177
        ProblemJson::from_status(
×
178
            StatusCode::UNAUTHORIZED,
×
179
            Some("Unauthorized".to_string()),
×
UNCOV
180
            Some(req.uri().to_string()),
×
181
        ),
182
    )
183
        .into_response()
184
}
185

UNCOV
186
pub fn build_router(
×
187
    state: AppState,
188
    jwt_credentials: Vec<(String, String, String)>,
189
    x402_config: Option<X402Config>,
190
) -> Router {
191
    // Public router (no auth middleware)
UNCOV
192
    let public_router = Router::new()
×
193
        .route("/v1/backups/{task_id}/download", get(handle_download))
×
UNCOV
194
        .merge(SwaggerUi::new("/v1/swagger-ui").url("/v1/openapi.json", ApiDoc::openapi()))
×
UNCOV
195
        .with_state(state.clone());
×
196

197
    // Authenticated router
198
    let mut authed_router = Router::new()
×
UNCOV
199
        .route("/v1/backups", get(handle_backups))
×
UNCOV
200
        .route("/v1/backups/{task_id}", get(handle_backup))
×
201
        .route(
202
            "/v1/backups/{task_id}/download-tokens",
203
            post(handle_download_token),
×
204
        )
UNCOV
205
        .route("/v1/backups/{task_id}/retries", post(handle_backup_retries))
×
206
        .route(
207
            "/v1/backups/{task_id}/archive",
UNCOV
208
            delete(handle_backup_delete_archive),
×
209
        )
210
        .route(
211
            "/v1/backups/{task_id}/pins",
212
            delete(handle_backup_delete_pins),
×
213
        )
UNCOV
214
        .route("/v1/pins", get(handle_pins))
×
UNCOV
215
        .with_state(state.clone());
×
216

217
    // Add POST /v1/backups route with optional x402 middleware
UNCOV
218
    let post_backups_route = match x402_config {
×
219
        None => Router::new()
×
220
            .route("/v1/backups", post(handle_backup_create))
×
221
            .with_state(state.clone()),
×
UNCOV
222
        Some(config) => {
×
UNCOV
223
            let x402 = config
×
224
                .to_middleware()
225
                .expect("invalid x402 middleware config")
226
                .with_description("Backup creation API");
227

228
            // TODO: make this configurable
UNCOV
229
            let price_tag = config
×
230
                .usdc_price_tag_for_amount("0.1")
231
                .expect("invalid x402 price");
232

UNCOV
233
            Router::new()
×
UNCOV
234
                .route("/v1/backups", post(handle_backup_create))
×
UNCOV
235
                .layer(x402.with_price_tag(price_tag))
×
UNCOV
236
                .with_state(state.clone())
×
237
        }
238
    };
239

UNCOV
240
    authed_router = authed_router.merge(post_backups_route);
×
241

242
    let auth_state = AuthState {
UNCOV
243
        app_state: state.clone(),
×
244
        jwt_credentials,
245
    };
UNCOV
246
    if is_defined(&state.auth_token) || !auth_state.jwt_credentials.is_empty() {
×
UNCOV
247
        authed_router =
×
UNCOV
248
            authed_router.layer(middleware::from_fn_with_state(auth_state, auth_middleware));
×
249
    }
250

UNCOV
251
    public_router.merge(authed_router)
×
252
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc