use axum::extract::State; use axum::Json; use crate::auth::{create_token, hash_password, verify_password, AuthUser}; use crate::config::AppState; use crate::errors::AppError; use crate::models::{ AuthResponse, LoginRequest, MeResponse, RegisterRequest, StatsResponse, UserResponse, }; // --------------------------------------------------------------------------- // POST /api/register // --------------------------------------------------------------------------- pub async fn register( State(state): State, Json(body): Json, ) -> Result, AppError> { if body.username.is_empty() || body.email.is_empty() || body.password.is_empty() { return Err(AppError::BadRequest( "username, email, and password are required".to_string(), )); } let id = uuid::Uuid::new_v4().to_string(); let password_hash = hash_password(&body.password); // Insert user sqlx::query("INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)") .bind(&id) .bind(&body.username) .bind(&body.email) .bind(&password_hash) .execute(&state.db) .await .map_err(|e| { if e.to_string().contains("UNIQUE") { AppError::BadRequest("Username or email already taken".to_string()) } else { AppError::Internal(e.to_string()) } })?; // Insert default user_stats row sqlx::query("INSERT INTO user_stats (user_id) VALUES (?)") .bind(&id) .execute(&state.db) .await?; let token = create_token(&id, &state.jwt_secret); Ok(Json(AuthResponse { token, user: UserResponse { id, username: body.username, email: body.email, }, })) } // --------------------------------------------------------------------------- // POST /api/login // --------------------------------------------------------------------------- pub async fn login( State(state): State, Json(body): Json, ) -> Result, AppError> { let row = sqlx::query_as::<_, (String, String, String, String)>( "SELECT id, username, email, password_hash FROM users WHERE email = ?", ) .bind(&body.email) .fetch_optional(&state.db) .await? .ok_or_else(|| AppError::Unauthorized("Invalid email or password".to_string()))?; let (id, username, email, password_hash) = row; if !verify_password(&body.password, &password_hash) { return Err(AppError::Unauthorized( "Invalid email or password".to_string(), )); } let token = create_token(&id, &state.jwt_secret); Ok(Json(AuthResponse { token, user: UserResponse { id, username, email, }, })) } // --------------------------------------------------------------------------- // GET /api/me // --------------------------------------------------------------------------- pub async fn me( State(state): State, AuthUser(user_id): AuthUser, ) -> Result, AppError> { let user = sqlx::query_as::<_, (String, String, String)>( "SELECT id, username, email FROM users WHERE id = ?", ) .bind(&user_id) .fetch_optional(&state.db) .await? .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; let stats = sqlx::query_as::<_, (i32, i32, i32, i32)>( "SELECT xp, streak_days, hearts, streak_freezes FROM user_stats WHERE user_id = ?", ) .bind(&user_id) .fetch_optional(&state.db) .await? .ok_or_else(|| AppError::NotFound("User stats not found".to_string()))?; Ok(Json(MeResponse { user: UserResponse { id: user.0, username: user.1, email: user.2, }, stats: StatsResponse { xp: stats.0, streak_days: stats.1, hearts: stats.2, streak_freezes: stats.3, }, })) }