use crate::middleware::{limit_concurrency, MakeRequestUuid}; use crate::{ config::Config, db::Pool, models::achievement::AchievementDefinition, models::chemistry_style::{load_chemistry_styles, ChemistryStyle}, models::event::EventDefinition, models::objective::{ObjectiveDefinition, ObjectiveType}, models::pack::PackDefinition, models::sbc::SbcDefinition, routes, services::{ achievement::load_achievement_definitions, card_db::CardDb, event::load_event_definitions, market, objective::load_objective_definitions, pack::load_pack_definitions, sbc::load_sbc_definitions, }, }; use anyhow::Result; use axum::{ extract::DefaultBodyLimit, middleware, routing::{delete, get, patch, post, put}, Router, }; use std::sync::Arc; use tower_http::{ cors::CorsLayer, request_id::{PropagateRequestIdLayer, SetRequestIdLayer}, trace::TraceLayer, }; #[derive(Clone)] pub struct AppState { pub pool: Pool, pub card_db: Arc, pub pack_defs: Arc>, pub obj_defs: Arc>, pub sbc_defs: Arc>, pub event_defs: Arc>, pub chem_styles: Arc>, pub achievement_defs: Arc>, } pub async fn build(pool: Pool, cfg: Config) -> Result { let card_db = Arc::new(CardDb::load(&cfg.data_dir)?); let pack_defs = Arc::new(load_pack_definitions(&cfg.data_dir)?); let obj_defs = Arc::new(load_objective_definitions(&cfg.data_dir)?); let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?); let event_defs = Arc::new(load_event_definitions(&cfg.data_dir)?); let chem_styles = Arc::new(load_chemistry_styles(&cfg.data_dir)?); let achievement_defs = Arc::new(load_achievement_definitions(&cfg.data_dir)?); tracing::info!( "Loaded {} cards, {} packs, {} objectives, {} SBCs, {} events, {} chemistry styles, {} achievements", card_db.cards.len(), pack_defs.len(), obj_defs.len(), sbc_defs.len(), event_defs.len(), chem_styles.len(), achievement_defs.len(), ); let state = AppState { pool: pool.clone(), card_db: card_db.clone(), pack_defs, obj_defs: obj_defs.clone(), sbc_defs, event_defs: event_defs.clone(), chem_styles, achievement_defs, }; // Background task: refresh NPC market at startup and every 24 hours { let pool = pool.clone(); let card_db = card_db.clone(); let event_defs_bg = event_defs.clone(); tokio::spawn(async move { if let Err(e) = market::refresh_npc_listings(&pool, &card_db, &event_defs_bg).await { tracing::warn!("initial market refresh failed: {e}"); } loop { tokio::time::sleep(tokio::time::Duration::from_secs(86_400)).await; if let Err(e) = market::refresh_npc_listings(&pool, &card_db, &event_defs_bg).await { tracing::warn!("market refresh failed: {e}"); } } }); } // Background task: reset daily objective progress at midnight UTC { let pool = pool.clone(); let daily_ids: Vec = obj_defs .iter() .filter(|d| matches!(d.objective_type, ObjectiveType::Daily)) .map(|d| d.id.clone()) .collect(); tokio::spawn(async move { loop { let now = chrono::Utc::now(); let tomorrow = now.date_naive() + chrono::Days::new(1); let midnight = tomorrow .and_hms_opt(0, 0, 0) .map(|dt| dt.and_utc()) .unwrap_or_else(|| now + chrono::Duration::hours(24)); let secs = (midnight - now).num_seconds().max(1) as u64; tokio::time::sleep(tokio::time::Duration::from_secs(secs)).await; for id in &daily_ids { if let Err(e) = sqlx::query( "UPDATE objective_progress SET current = 0, completed = 0, claimed = 0 WHERE objective_id = ?", ) .bind(id) .execute(&pool) .await { tracing::warn!("daily reset failed for {id}: {e}"); } } tracing::info!("Daily objectives reset ({} objectives)", daily_ids.len()); } }); } let router = Router::new() .route("/health", get(routes::health::get_health)) .route("/formations", get(routes::health::get_formations)) .route("/auth/local", post(routes::auth::post_auth_local)) .route("/auth/status", get(routes::auth::get_auth_status)) .route("/auth/reset", post(routes::auth::post_auth_reset)) .route("/profile", get(routes::profile::get_profile)) .route("/club", get(routes::club::get_club)) .route("/club", put(routes::club::put_club)) .route("/club/checkin", get(routes::club::get_checkin_status)) .route("/club/checkin", post(routes::club::post_checkin)) .route("/club/milestones", get(routes::club::get_milestones)) .route("/cards", get(routes::cards::get_cards)) .route("/cards/:card_id", get(routes::cards::get_card)) .route("/collection", get(routes::cards::get_collection)) .route( "/collection/:owned_card_id", delete(routes::cards::delete_owned_card), ) .route( "/collection/:owned_card_id/chemistry-style", post(routes::upgrades::post_apply_chemistry_style), ) .route( "/collection/:owned_card_id/position", post(routes::upgrades::post_change_position), ) .route( "/collection/:owned_card_id/training", post(routes::upgrades::post_apply_training), ) .route( "/chemistry-styles", get(routes::upgrades::get_chemistry_styles), ) .route("/packs", get(routes::packs::get_packs)) .route("/packs/store", get(routes::packs::get_pack_store)) .route("/packs/history", get(routes::packs::get_pack_history)) .route("/packs/buy", post(routes::packs::post_buy_pack)) .route("/packs/open/:pack_id", post(routes::packs::post_open_pack)) .route("/squad", get(routes::squad::get_squad)) .route("/squad", post(routes::squad::post_squad)) .route("/squads", get(routes::squad::get_squads)) .route("/squads/:squad_id", get(routes::squad::get_squad_by_id)) .route("/squads/:squad_id", delete(routes::squad::delete_squad)) .route("/objectives", get(routes::objectives::get_objectives)) .route("/objectives/:objective_id", get(routes::objectives::get_objective)) .route( "/objectives/claim", post(routes::objectives::post_claim_objective), ) .route( "/objectives/:objective_id/claim", post(routes::objectives::post_claim_objective_by_id), ) .route("/matches", get(routes::matches::get_matches)) .route("/matches/opponent", get(routes::matches::get_opponent)) .route("/matches/result", post(routes::matches::post_match_result)) .route("/sbc", get(routes::sbc::get_sbcs)) .route("/sbc/submit", post(routes::sbc::post_sbc_submit)) .route("/sbc/:sbc_id", get(routes::sbc::get_sbc)) .route("/market", get(routes::market::get_market)) .route("/market/buy", post(routes::market::post_market_buy)) .route("/market/sell", post(routes::market::post_market_sell)) .route("/market/trade-history", get(routes::market::get_trade_history)) .route("/market/refresh", post(routes::market::post_market_refresh)) .route("/market/my-listings", get(routes::market::get_my_listings)) .route( "/market/listings/:listing_id", delete(routes::market::delete_market_listing), ) .route("/statistics", get(routes::statistics::get_statistics)) .route( "/statistics/history", get(routes::statistics::get_statistics_history), ) .route("/settings", get(routes::settings::get_settings)) .route("/settings", put(routes::settings::put_settings)) .route("/division", get(routes::division::get_division)) .route("/division/history", get(routes::division::get_division_history)) .route("/division/leaderboard", get(routes::division::get_division_leaderboard)) .route("/achievements", get(routes::achievements::get_achievements)) .route("/notifications", get(routes::notifications::get_notifications)) .route("/notifications/read-all", post(routes::notifications::mark_all_notifications_read)) .route("/notifications/:id/read", patch(routes::notifications::mark_notification_read)) .route("/fut-champs", get(routes::fut_champs::get_fut_champs)) .route("/fut-champs/start", post(routes::fut_champs::post_start_fut_champs)) .route("/fut-champs/history", get(routes::fut_champs::get_champs_history)) .route( "/fut-champs/:session_id/result", post(routes::fut_champs::post_champs_result), ) .route( "/fut-champs/:session_id/claim", post(routes::fut_champs::post_claim_champs_rewards), ) .route( "/rivals/claim-weekly", post(routes::fut_champs::post_claim_rivals_reward), ) .route("/draft/squad", get(routes::draft::get_draft_squad)) .route("/draft/start", post(routes::draft::post_draft_start)) .route("/draft/sessions/:id", get(routes::draft::get_draft_session)) .route( "/draft/sessions/:id/pick", post(routes::draft::post_draft_pick), ) .route( "/draft/sessions/:id/abandon", post(routes::draft::post_draft_abandon), ) .route("/events", get(routes::events::get_events)) .route("/events/:event_id", get(routes::events::get_event)) .route( "/events/:event_id/activate", post(routes::events::post_activate_event), ) .route( "/events/:event_id/deactivate", post(routes::events::post_deactivate_event), ) // Layers run outermost-first (last added = outermost). // CORS → body limit → concurrency limit → request ID → trace .layer(PropagateRequestIdLayer::x_request_id()) .layer(TraceLayer::new_for_http()) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) .layer(middleware::from_fn(limit_concurrency)) .layer(DefaultBodyLimit::max(256 * 1024)) .layer(CorsLayer::permissive()) .with_state(state); Ok(router) }