this repo has no description
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}