this repo has no description
at fix-ts-uint8array 135 lines 4.1 kB view raw
1use axum::extract::State; 2use axum::Json; 3 4use crate::auth::{create_token, hash_password, verify_password, AuthUser}; 5use crate::config::AppState; 6use crate::errors::AppError; 7use crate::models::{ 8 AuthResponse, LoginRequest, MeResponse, RegisterRequest, StatsResponse, UserResponse, 9}; 10 11// --------------------------------------------------------------------------- 12// POST /api/register 13// --------------------------------------------------------------------------- 14 15pub async fn register( 16 State(state): State<AppState>, 17 Json(body): Json<RegisterRequest>, 18) -> Result<Json<AuthResponse>, AppError> { 19 if body.username.is_empty() || body.email.is_empty() || body.password.is_empty() { 20 return Err(AppError::BadRequest( 21 "username, email, and password are required".to_string(), 22 )); 23 } 24 25 let id = uuid::Uuid::new_v4().to_string(); 26 let password_hash = hash_password(&body.password); 27 28 // Insert user 29 sqlx::query("INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)") 30 .bind(&id) 31 .bind(&body.username) 32 .bind(&body.email) 33 .bind(&password_hash) 34 .execute(&state.db) 35 .await 36 .map_err(|e| { 37 if e.to_string().contains("UNIQUE") { 38 AppError::BadRequest("Username or email already taken".to_string()) 39 } else { 40 AppError::Internal(e.to_string()) 41 } 42 })?; 43 44 // Insert default user_stats row 45 sqlx::query("INSERT INTO user_stats (user_id) VALUES (?)") 46 .bind(&id) 47 .execute(&state.db) 48 .await?; 49 50 let token = create_token(&id, &state.jwt_secret); 51 52 Ok(Json(AuthResponse { 53 token, 54 user: UserResponse { 55 id, 56 username: body.username, 57 email: body.email, 58 }, 59 })) 60} 61 62// --------------------------------------------------------------------------- 63// POST /api/login 64// --------------------------------------------------------------------------- 65 66pub async fn login( 67 State(state): State<AppState>, 68 Json(body): Json<LoginRequest>, 69) -> Result<Json<AuthResponse>, AppError> { 70 let row = sqlx::query_as::<_, (String, String, String, String)>( 71 "SELECT id, username, email, password_hash FROM users WHERE email = ?", 72 ) 73 .bind(&body.email) 74 .fetch_optional(&state.db) 75 .await? 76 .ok_or_else(|| AppError::Unauthorized("Invalid email or password".to_string()))?; 77 78 let (id, username, email, password_hash) = row; 79 80 if !verify_password(&body.password, &password_hash) { 81 return Err(AppError::Unauthorized( 82 "Invalid email or password".to_string(), 83 )); 84 } 85 86 let token = create_token(&id, &state.jwt_secret); 87 88 Ok(Json(AuthResponse { 89 token, 90 user: UserResponse { 91 id, 92 username, 93 email, 94 }, 95 })) 96} 97 98// --------------------------------------------------------------------------- 99// GET /api/me 100// --------------------------------------------------------------------------- 101 102pub async fn me( 103 State(state): State<AppState>, 104 AuthUser(user_id): AuthUser, 105) -> Result<Json<MeResponse>, AppError> { 106 let user = sqlx::query_as::<_, (String, String, String)>( 107 "SELECT id, username, email FROM users WHERE id = ?", 108 ) 109 .bind(&user_id) 110 .fetch_optional(&state.db) 111 .await? 112 .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; 113 114 let stats = sqlx::query_as::<_, (i32, i32, i32, i32)>( 115 "SELECT xp, streak_days, hearts, streak_freezes FROM user_stats WHERE user_id = ?", 116 ) 117 .bind(&user_id) 118 .fetch_optional(&state.db) 119 .await? 120 .ok_or_else(|| AppError::NotFound("User stats not found".to_string()))?; 121 122 Ok(Json(MeResponse { 123 user: UserResponse { 124 id: user.0, 125 username: user.1, 126 email: user.2, 127 }, 128 stats: StatsResponse { 129 xp: stats.0, 130 streak_days: stats.1, 131 hearts: stats.2, 132 streak_freezes: stats.3, 133 }, 134 })) 135}