Initial commit: OpenFUT Core

Offline Ultimate Team backend — game-independent REST API.

- 19 API endpoints: auth, profiles, clubs, cards, packs, squads,
  objectives, SBCs, match rewards, NPC market, statistics
- Axum + SQLite + SQLx with full migrations
- Weighted pack generator, SBC validation engine
- JSON-driven mod data (cards, packs, objectives, SBCs)
- 5 integration tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 14:52:06 -07:00
commit 1ffe0ffa9f
60 changed files with 6152 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
use crate::{
db::Pool,
error::{AppError, AppResult},
models::profile::Profile,
};
use chrono::Utc;
pub async fn get_active_profile(pool: &Pool) -> AppResult<Profile> {
sqlx::query_as::<_, Profile>(
"SELECT id, username, level, xp, created_at, updated_at FROM profiles ORDER BY created_at ASC LIMIT 1"
)
.fetch_optional(pool)
.await?
.ok_or_else(|| AppError::NotFound("no profile exists; call POST /auth/local first".into()))
}
pub async fn create_profile(pool: &Pool, username: &str) -> AppResult<Profile> {
let existing = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM profiles")
.fetch_one(pool)
.await?;
if existing > 0 {
return Err(AppError::Conflict(
"a profile already exists; OpenFUT is single-player only".into(),
));
}
let profile = Profile::new(username);
sqlx::query(
"INSERT INTO profiles (id, username, level, xp, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
)
.bind(&profile.id)
.bind(&profile.username)
.bind(profile.level)
.bind(profile.xp)
.bind(profile.created_at)
.bind(profile.updated_at)
.execute(pool)
.await?;
Ok(profile)
}
pub async fn add_xp(pool: &Pool, profile_id: &str, xp: i64) -> AppResult<()> {
let now = Utc::now();
sqlx::query("UPDATE profiles SET xp = xp + ?, updated_at = ? WHERE id = ?")
.bind(xp)
.bind(now)
.bind(profile_id)
.execute(pool)
.await?;
Ok(())
}