wip: checkpoint multi-game core work

This commit is contained in:
funman300
2026-08-07 12:03:21 -07:00
parent 11a811db6a
commit 8c8a4116bf
41 changed files with 405 additions and 180 deletions
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{extract::State, Json};
use serde_json::{json, Value};
@@ -7,8 +8,8 @@ use crate::{
services::{achievement as ach_svc, club as club_svc, profile as profile_svc},
};
pub async fn get_achievements(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_achievements(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let _ = ach_svc::check_and_unlock(&state.pool, &state.achievement_defs, &profile.id, &club.id).await;
let achievements = ach_svc::list_with_status(&state.pool, &state.achievement_defs).await?;
+85 -28
View File
@@ -1,9 +1,11 @@
use axum::{extract::State, Json};
use serde::Deserialize;
use serde_json::{json, Value};
use crate::{
app::AppState,
error::AppResult,
error::{AppError, AppResult},
extractors::GameId,
models::{club::Club, profile::CreateProfileRequest},
seed,
services::{club as club_svc, profile as profile_svc},
@@ -11,11 +13,12 @@ use crate::{
pub async fn post_auth_local(
State(state): State<AppState>,
game: GameId,
Json(req): Json<CreateProfileRequest>,
) -> AppResult<Json<Value>> {
let username = req.username.unwrap_or_else(|| "Player 1".into());
let profile = profile_svc::create_profile(&state.pool, &username).await?;
let profile = profile_svc::create_profile(&state.pool, &username, game.as_str()).await?;
let club = Club::new(&profile.id, "OpenFUT FC", 5000);
club_svc::create_club(&state.pool, &club).await?;
@@ -31,51 +34,105 @@ pub async fn post_auth_local(
/// GET /auth/status — lightweight check: does a profile exist?
/// Returns 200 `{ "has_profile": true/false }` without erroring.
pub async fn get_auth_status(State(state): State<AppState>) -> AppResult<Json<Value>> {
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM profiles")
pub async fn get_auth_status(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM profiles WHERE game_id = ?")
.bind(game.as_str())
.fetch_one(&state.pool)
.await?;
Ok(Json(json!({ "has_profile": count > 0 })))
}
#[derive(Deserialize)]
pub struct ResetRequest {
pub confirm: Option<String>,
}
/// POST /auth/reset — wipe all game data and start fresh.
/// Deletes every user-data table in dependency order. The schema tables
/// (migrations) are left intact; calling POST /auth/local afterwards
/// creates a new profile.
pub async fn post_auth_reset(State(state): State<AppState>) -> AppResult<Json<Value>> {
// Delete in reverse-dependency order to satisfy FK constraints
// (SQLite FK enforcement is opt-in, but we follow the order anyway)
let tables = [
"player_achievements",
"notifications",
/// Requires `{"confirm":"reset"}` in the request body as a safeguard against
/// accidental or unauthenticated calls. Deletes every user-data table in
/// dependency order; schema (migrations) is preserved.
pub async fn post_auth_reset(
State(state): State<AppState>,
game: GameId,
Json(req): Json<ResetRequest>,
) -> AppResult<Json<Value>> {
if req.confirm.as_deref() != Some("reset") {
return Err(AppError::BadRequest(
r#"include {"confirm":"reset"} in the request body to confirm data wipe"#.into(),
));
}
// Multi-game: reset ONLY this game's profile subtree so a FIFA 17 reset never
// wipes FIFA 23 (and vice versa). Resolve the game's profile + its clubs, then
// delete their dependent rows in reverse-dependency order.
let profile_id: Option<String> = sqlx::query_scalar(
"SELECT id FROM profiles WHERE game_id = ? ORDER BY created_at ASC LIMIT 1",
)
.bind(game.as_str())
.fetch_optional(&state.pool)
.await?;
let Some(profile_id) = profile_id else {
return Ok(Json(json!({
"reset": true,
"message": "nothing to reset for this game"
})));
};
let club_ids: Vec<String> = sqlx::query_scalar("SELECT id FROM clubs WHERE profile_id = ?")
.bind(&profile_id)
.fetch_all(&state.pool)
.await?;
for club_id in &club_ids {
sqlx::query(
"DELETE FROM squad_players WHERE squad_id IN (SELECT id FROM squads WHERE club_id = ?)",
)
.bind(club_id)
.execute(&state.pool)
.await?;
for table in ["squads", "owned_cards", "packs", "market_history"] {
sqlx::query(&format!("DELETE FROM {table} WHERE club_id = ?"))
.bind(club_id)
.execute(&state.pool)
.await?;
}
}
for table in [
"fut_champs_sessions",
"sbc_submissions",
"objective_progress",
"position_goals",
"statistics",
"seasons",
"season_history",
"matches",
"market_listings",
"squad_players",
"squads",
"owned_cards",
"packs",
"events",
"draft_sessions",
"settings",
"clubs",
"profiles",
];
for table in &tables {
sqlx::query(&format!("DELETE FROM {table}"))
"daily_checkins",
] {
sqlx::query(&format!("DELETE FROM {table} WHERE profile_id = ?"))
.bind(&profile_id)
.execute(&state.pool)
.await?;
}
tracing::info!("Full game reset performed");
sqlx::query("DELETE FROM clubs WHERE profile_id = ?")
.bind(&profile_id)
.execute(&state.pool)
.await?;
sqlx::query("DELETE FROM profiles WHERE id = ?")
.bind(&profile_id)
.execute(&state.pool)
.await?;
// NOTE (follow-up): settings (global key/value), events (shared content),
// market_listings (keyed by seller_name, includes the shared NPC market),
// notifications and player_achievements are not yet game-scoped and are left
// intact. They need a game/profile key before a per-game reset can cover them.
tracing::info!(game = %game.as_str(), "Per-game reset performed");
Ok(Json(json!({
"reset": true,
"message": "All progress has been wiped. Call POST /auth/local to start a new club."
"message": "All progress for this game has been wiped. Call POST /auth/local to start a new club."
})))
}
+6 -4
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, Query, State},
Json,
@@ -56,7 +57,7 @@ pub async fn get_cards(
let rarity_ok = query
.rarity
.as_ref()
.map(|r| format!("{:?}", c.rarity).to_lowercase() == r.to_lowercase())
.map(|r| c.rarity.as_str().eq_ignore_ascii_case(r))
.unwrap_or(true);
let pos_ok = query
.position
@@ -93,8 +94,8 @@ pub async fn get_cards(
Ok(Json(json!({ "cards": cards, "total": total, "returned": cards.len() })))
}
pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_collection(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let owned = sqlx::query_as::<_, OwnedCard>(
@@ -135,9 +136,10 @@ pub async fn get_collection(State(state): State<AppState>) -> AppResult<Json<Val
/// Quick-sell an owned card for instant coins. The card is removed from the collection.
pub async fn delete_owned_card(
State(state): State<AppState>,
game: GameId,
Path(owned_card_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let owned = sqlx::query_as::<_, OwnedCard>(
+11 -9
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use crate::{
app::AppState,
error::AppResult,
@@ -8,8 +9,8 @@ use axum::{extract::State, Json};
use serde::Deserialize;
use serde_json::{json, Value};
pub async fn get_club(State(state): State<AppState>) -> AppResult<Json<Club>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_club(State(state): State<AppState>, game: GameId) -> AppResult<Json<Club>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
Ok(Json(club))
}
@@ -22,9 +23,10 @@ pub struct UpdateClubRequest {
pub async fn put_club(
State(state): State<AppState>,
game: GameId,
Json(req): Json<UpdateClubRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated = club_svc::update_club(
&state.pool,
@@ -36,8 +38,8 @@ pub async fn put_club(
Ok(Json(json!({ "club": updated })))
}
pub async fn get_checkin_status(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_checkin_status(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let status = checkin_svc::get_status(&state.pool, &profile.id).await?;
Ok(Json(json!({
"available": status.available,
@@ -48,8 +50,8 @@ pub async fn get_checkin_status(State(state): State<AppState>) -> AppResult<Json
})))
}
pub async fn post_checkin(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn post_checkin(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let r = checkin_svc::claim(&state.pool, &profile.id, &club.id).await?;
Ok(Json(json!({
@@ -60,8 +62,8 @@ pub async fn post_checkin(State(state): State<AppState>) -> AppResult<Json<Value
})))
}
pub async fn get_milestones(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_milestones(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let stats = stats_svc::get_or_create(&state.pool, &profile.id).await?;
+7 -6
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{extract::State, Json};
use rand::{Rng, SeedableRng};
use serde_json::{json, Value};
@@ -9,8 +10,8 @@ use crate::{
services::{club as club_svc, profile as profile_svc, season as season_svc},
};
pub async fn get_division(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_division(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let _club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let season = season_svc::get_or_create(&state.pool, &profile.id).await?;
@@ -36,14 +37,14 @@ pub async fn get_division(State(state): State<AppState>) -> AppResult<Json<Value
})))
}
pub async fn get_division_history(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_division_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let history = season_svc::get_history(&state.pool, &profile.id).await?;
Ok(Json(json!({ "history": history, "total": history.len() })))
}
pub async fn get_division_leaderboard(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_division_leaderboard(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let season = season_svc::get_or_create(&state.pool, &profile.id).await?;
+9 -4
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, Query, State},
Json,
@@ -39,9 +40,10 @@ pub async fn get_draft_squad(
/// until all 11 slots are filled.
pub async fn post_draft_start(
State(state): State<AppState>,
game: GameId,
Query(query): Query<DraftQuery>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let difficulty = query.difficulty.as_deref().unwrap_or("professional");
let session = draft_svc::start_draft(&state.pool, &state.card_db, &profile.id, difficulty).await?;
Ok(Json(session))
@@ -50,9 +52,10 @@ pub async fn post_draft_start(
/// Get the current state of a draft session.
pub async fn get_draft_session(
State(state): State<AppState>,
game: GameId,
Path(session_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let session = draft_svc::get_draft(&state.pool, &state.card_db, &profile.id, &session_id).await?;
Ok(Json(session))
}
@@ -69,10 +72,11 @@ pub struct PickRequest {
/// and rewards (coins + optional pack) are granted automatically.
pub async fn post_draft_pick(
State(state): State<AppState>,
game: GameId,
Path(session_id): Path<String>,
Json(req): Json<PickRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let session = draft_svc::pick_card(
&state.pool,
@@ -89,9 +93,10 @@ pub async fn post_draft_pick(
/// Abandon an active draft session. No rewards are granted.
pub async fn post_draft_abandon(
State(state): State<AppState>,
game: GameId,
Path(session_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let result = draft_svc::abandon_draft(&state.pool, &profile.id, &session_id).await?;
Ok(Json(result))
}
+13 -10
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -12,8 +13,8 @@ use crate::{
};
/// GET /fut-champs — current active session, or null if none.
pub async fn get_fut_champs(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_fut_champs(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let session = champs_svc::get_active_session(&state.pool, &profile.id).await?;
Ok(Json(json!({
@@ -23,8 +24,8 @@ pub async fn get_fut_champs(State(state): State<AppState>) -> AppResult<Json<Val
}
/// POST /fut-champs/start — open a new FUT Champions week.
pub async fn post_start_fut_champs(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn post_start_fut_champs(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let session = champs_svc::start_session(&state.pool, &profile.id).await?;
Ok(Json(json!({
@@ -42,10 +43,11 @@ pub struct ChampsMatchRequest {
/// POST /fut-champs/:session_id/result — record a match in this session.
pub async fn post_champs_result(
State(state): State<AppState>,
game: GameId,
Path(session_id): Path<String>,
Json(req): Json<ChampsMatchRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let session = champs_svc::record_match(
&state.pool,
@@ -75,9 +77,10 @@ pub async fn post_champs_result(
/// POST /fut-champs/:session_id/claim — claim end-of-week rewards.
pub async fn post_claim_champs_rewards(
State(state): State<AppState>,
game: GameId,
Path(session_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let result = champs_svc::claim_rewards(
@@ -93,8 +96,8 @@ pub async fn post_claim_champs_rewards(
}
/// GET /fut-champs/history — past sessions, newest first.
pub async fn get_champs_history(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_champs_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let history = champs_svc::get_history(&state.pool, &profile.id).await?;
Ok(Json(json!({
@@ -104,8 +107,8 @@ pub async fn get_champs_history(State(state): State<AppState>) -> AppResult<Json
}
/// POST /rivals/claim-weekly — claim weekly Division Rivals reward.
pub async fn post_claim_rivals_reward(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn post_claim_rivals_reward(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
// Ensure a season row exists
+11 -7
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, Query, State},
Json,
@@ -12,8 +13,8 @@ use crate::{
services::{club as club_svc, market as market_svc, profile as profile_svc},
};
pub async fn get_trade_history(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_trade_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let trades = market_svc::get_trade_history(&state.pool, &club.id).await?;
Ok(Json(json!({ "trades": trades, "total": trades.len() })))
@@ -52,9 +53,10 @@ pub async fn get_market(
pub async fn post_market_buy(
State(state): State<AppState>,
game: GameId,
Json(req): Json<BuyListingRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let card = market_svc::buy_listing(&state.pool, &state.card_db, &club.id, &req).await?;
@@ -65,9 +67,10 @@ pub async fn post_market_buy(
pub async fn post_market_sell(
State(state): State<AppState>,
game: GameId,
Json(req): Json<SellCardRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let new_balance = market_svc::sell_card(&state.pool, &state.card_db, &club.id, &req).await?;
@@ -83,8 +86,8 @@ pub async fn post_market_refresh(State(state): State<AppState>) -> AppResult<Jso
}
/// Return all active market listings posted by the current player's club.
pub async fn get_my_listings(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_my_listings(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let listings = market_svc::get_listings_by_seller(&state.pool, &state.card_db, &club.id).await?;
Ok(Json(json!({ "listings": listings, "total": listings.len() })))
@@ -93,9 +96,10 @@ pub async fn get_my_listings(State(state): State<AppState>) -> AppResult<Json<Va
/// Cancel a player-posted listing and return the card to the collection.
pub async fn delete_market_listing(
State(state): State<AppState>,
game: GameId,
Path(listing_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
market_svc::cancel_listing(&state.pool, &club.id, &listing_id).await?;
Ok(Json(json!({ "cancelled": listing_id })))
+5 -2
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Query, State},
Json,
@@ -20,9 +21,10 @@ pub struct MatchHistoryQuery {
pub async fn get_matches(
State(state): State<AppState>,
game: GameId,
Query(query): Query<MatchHistoryQuery>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let limit = query.limit.unwrap_or(20).clamp(1, 100);
let matches = if let Some(mode) = &query.mode {
@@ -63,9 +65,10 @@ pub async fn get_opponent(
pub async fn post_match_result(
State(state): State<AppState>,
game: GameId,
Json(req): Json<SubmitMatchRequest>,
) -> AppResult<Json<MatchRewardResult>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let result =
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -16,8 +17,8 @@ use crate::{
/// notifications (unclaimed objectives, expiring loans, season ending soon).
/// Persistent notifications carry an `id` and `is_read` flag; dynamic ones
/// have `id: null` and are always considered unread.
pub async fn get_notifications(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_notifications(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
// ── Persistent notifications ─────────────────────────────────────────────
+9 -5
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -11,8 +12,8 @@ use crate::{
services::{club as club_svc, objective as obj_svc, profile as profile_svc},
};
pub async fn get_objectives(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_objectives(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let objectives =
obj_svc::get_objectives_with_progress(&state.pool, &profile.id, &state.obj_defs).await?;
Ok(Json(json!({ "objectives": objectives })))
@@ -20,9 +21,10 @@ pub async fn get_objectives(State(state): State<AppState>) -> AppResult<Json<Val
pub async fn get_objective(
State(state): State<AppState>,
game: GameId,
Path(objective_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let all =
obj_svc::get_objectives_with_progress(&state.pool, &profile.id, &state.obj_defs).await?;
let obj = all
@@ -34,9 +36,10 @@ pub async fn get_objective(
pub async fn post_claim_objective_by_id(
State(state): State<AppState>,
game: GameId,
Path(objective_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let reward = obj_svc::claim_objective(
&state.pool,
@@ -56,9 +59,10 @@ pub struct ClaimRequest {
pub async fn post_claim_objective(
State(state): State<AppState>,
game: GameId,
Json(req): Json<ClaimRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let reward = obj_svc::claim_objective(
&state.pool,
+9 -6
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -19,9 +20,10 @@ pub struct BuyPackRequest {
pub async fn post_buy_pack(
State(state): State<AppState>,
game: GameId,
Json(req): Json<BuyPackRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let pack = pack_svc::buy_pack(
&state.pool,
@@ -54,8 +56,8 @@ pub async fn get_pack_store(State(state): State<AppState>) -> AppResult<Json<Val
Ok(Json(json!({ "packs": store })))
}
pub async fn get_packs(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_packs(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let packs = pack_svc::get_unopened_packs(&state.pool, &club.id).await?;
@@ -77,8 +79,8 @@ pub async fn get_packs(State(state): State<AppState>) -> AppResult<Json<Value>>
}
/// Return recently opened packs with the card IDs they contained.
pub async fn get_pack_history(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_pack_history(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let opened = sqlx::query_as::<_, crate::models::pack::Pack>(
@@ -122,9 +124,10 @@ pub async fn get_pack_history(State(state): State<AppState>) -> AppResult<Json<V
pub async fn post_open_pack(
State(state): State<AppState>,
game: GameId,
Path(pack_id): Path<String>,
) -> AppResult<Json<PackOpenResult>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let result = pack_svc::open_pack(
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use crate::{
app::AppState,
error::AppResult,
@@ -7,8 +8,8 @@ use crate::{
use axum::{extract::State, Json};
use serde_json::{json, Value};
pub async fn get_profile(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_profile(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let computed_level = level_for_xp(profile.xp);
let next_level = computed_level + 1;
+3 -1
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -29,9 +30,10 @@ pub async fn get_sbc(
pub async fn post_sbc_submit(
State(state): State<AppState>,
game: GameId,
Json(req): Json<SubmitSbcRequest>,
) -> AppResult<Json<SbcResult>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let result = sbc_svc::submit_sbc(
+12 -8
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -11,8 +12,8 @@ use crate::{
services::{club as club_svc, profile as profile_svc, squad as squad_svc},
};
pub async fn get_squad(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_squad(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let (squad, players) = squad_svc::get_squad(&state.pool, &club.id).await?;
@@ -21,8 +22,8 @@ pub async fn get_squad(State(state): State<AppState>) -> AppResult<Json<Value>>
Ok(Json(squad_response(&squad, &players, chemistry)))
}
pub async fn get_squads(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_squads(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let squads = squad_svc::list_squads(&state.pool, &club.id).await?;
Ok(Json(json!({ "squads": squads })))
@@ -30,9 +31,10 @@ pub async fn get_squads(State(state): State<AppState>) -> AppResult<Json<Value>>
pub async fn get_squad_by_id(
State(state): State<AppState>,
game: GameId,
Path(squad_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let (squad, players) = squad_svc::get_squad_by_id(&state.pool, &club.id, &squad_id).await?;
@@ -43,13 +45,14 @@ pub async fn get_squad_by_id(
pub async fn post_squad(
State(state): State<AppState>,
game: GameId,
Json(req): Json<SaveSquadRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
if !req.players.is_empty() {
squad_svc::validate_formation(&state.pool, &state.card_db, &req.players).await?;
squad_svc::validate_formation(&state.pool, &state.card_db, &club.id, &req.players).await?;
}
let squad = squad_svc::save_squad(&state.pool, &club.id, &req).await?;
@@ -58,9 +61,10 @@ pub async fn post_squad(
pub async fn delete_squad(
State(state): State<AppState>,
game: GameId,
Path(squad_id): Path<String>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
squad_svc::delete_squad(&state.pool, &club.id, &squad_id).await?;
Ok(Json(json!({ "deleted": squad_id })))
+5 -3
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Query, State},
Json,
@@ -12,8 +13,8 @@ use crate::{
services::{profile as profile_svc, statistics as stats_svc},
};
pub async fn get_statistics(State(state): State<AppState>) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
pub async fn get_statistics(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let stats = stats_svc::get_or_create(&state.pool, &profile.id).await?;
let pos_goals = stats_svc::get_position_goals(&state.pool, &profile.id).await?;
@@ -36,9 +37,10 @@ pub struct HistoryQuery {
pub async fn get_statistics_history(
State(state): State<AppState>,
game: GameId,
Query(query): Query<HistoryQuery>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let limit = query.limit.unwrap_or(20).clamp(1, 100);
let matches = sqlx::query_as::<_, Match>(
+7 -3
View File
@@ -1,3 +1,4 @@
use crate::extractors::GameId;
use axum::{
extract::{Path, State},
Json,
@@ -27,10 +28,11 @@ pub struct ApplyChemStyleRequest {
/// POST /collection/:owned_card_id/chemistry-style
pub async fn post_apply_chemistry_style(
State(state): State<AppState>,
game: GameId,
Path(owned_card_id): Path<String>,
Json(req): Json<ApplyChemStyleRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated = upgrade_svc::apply_chemistry_style(
@@ -63,10 +65,11 @@ pub struct ChangePositionRequest {
/// POST /collection/:owned_card_id/position — costs 500 coins.
pub async fn post_change_position(
State(state): State<AppState>,
game: GameId,
Path(owned_card_id): Path<String>,
Json(req): Json<ChangePositionRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated = upgrade_svc::change_position(
@@ -97,10 +100,11 @@ pub struct ApplyTrainingRequest {
/// POST /collection/:owned_card_id/training — applies a training boost (up to +3 OVR total).
pub async fn post_apply_training(
State(state): State<AppState>,
game: GameId,
Path(owned_card_id): Path<String>,
Json(req): Json<ApplyTrainingRequest>,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool).await?;
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let updated =