use std::time::{Duration, Instant}; use axum::http::{StatusCode, header}; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::{Json, Router}; use tower::ServiceBuilder; use tower_http::{cors::{Any, CorsLayer}, trace::TraceLayer}; use crate::card::{CardPresentResult, CardPresenter}; use crate::config::Config; use crate::metrics::SystemMetrics; use crate::utils; use crate::webapi::API_VERSION; pub fn build_server() -> Router { let services = ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer(CorsLayer::new().allow_origin(Any).allow_methods(Any)); Router::<()>::new() .route("/amnet/info", get(server_state_handler)) .route("/amnet/signin", post(card_submission_handler)) .layer(services) } fn build_error_response(status: StatusCode, message: &str) -> Response { let error = crate::webapi::ServerError { error: message.to_string(), }; (status, Json(error)).into_response() } async fn server_state_handler() -> Json { let config = Config::instance(); let metrics = SystemMetrics::instance(); let session_uptime = metrics.get_server_uptime().map(|d| d.as_secs()).unwrap_or(0); let time_since_last_poll = metrics.get_last_poll().map(|d| d.as_secs()); let state = crate::webapi::ServerState { api_version: API_VERSION, game_id: config.game_id.clone(), server_name: config.server_name.clone(), session_uptime, time_since_last_poll, }; Json(state) } async fn card_submission_handler( Json(payload): Json, ) -> impl IntoResponse { if payload.access_code.len() > 20 || payload.access_code.len() == 0 { return build_error_response(StatusCode::BAD_REQUEST, "Access code is required"); } let Some(access_code) = utils::parse_access_code(&payload.access_code) else { return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid access code provided"); }; let card_idm_bytes = if let Some(idm_hex) = payload.card_idm_hex.as_ref() { if idm_hex.len() != 16 { return build_error_response(StatusCode::BAD_REQUEST, "IDm must be a 16-char hex string"); } let Some(idm_bytes) = utils::parse_idm_hex(idm_hex) else { return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid IDm provided"); }; Some(idm_bytes) } else { None }; match CardPresenter::instance().present_card(access_code, card_idm_bytes, Duration::from_secs(5)) { CardPresentResult::Accepted => StatusCode::ACCEPTED.into_response(), CardPresentResult::SlotUnavailable(release_time) => { let now = Instant::now(); let wait_secs = if release_time > now { (release_time - now).as_secs() } else { 0 }; let mut response = build_error_response( StatusCode::TOO_MANY_REQUESTS, "A card has already been submitted. Please wait before submitting another.", ); let headers = response.headers_mut(); headers.insert(header::RETRY_AFTER, wait_secs.to_string().parse().unwrap()); response } } }