use axum::{extract::State, Json}; use serde::Deserialize; use serde_json::{json, Value}; use crate::{ app::AppState, error::{AppError, AppResult}, extractors::GameId, models::{club::Club, profile::CreateProfileRequest}, seed, services::{club as club_svc, profile as profile_svc}, }; pub async fn post_auth_local( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let username = req.username.unwrap_or_else(|| "Player 1".into()); 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?; seed::grant_starter_pack(&state.pool, &club.id, &state.pack_defs).await?; Ok(Json(json!({ "profile": profile, "club": club, "message": "Welcome to OpenFUT FC! Your club has been created." }))) } /// 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, game: GameId) -> AppResult> { 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, } /// POST /auth/reset — wipe all game data and start fresh. /// 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, game: GameId, Json(req): Json, ) -> AppResult> { 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 = 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 = 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", "draft_sessions", "daily_checkins", ] { sqlx::query(&format!("DELETE FROM {table} WHERE profile_id = ?")) .bind(&profile_id) .execute(&state.pool) .await?; } 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 for this game has been wiped. Call POST /auth/local to start a new club." }))) }