ares/api/handlers/
auth.rs1use crate::{
2 db::traits::DatabaseClient,
3 types::{AppError, LoginRequest, RegisterRequest, Result, TokenResponse},
4 AppState,
5};
6use axum::{extract::State, Json};
7use serde::Deserialize;
8use utoipa::ToSchema;
9use uuid::Uuid;
10
11#[derive(Debug, Deserialize, ToSchema)]
13pub struct RefreshTokenRequest {
14 pub refresh_token: String,
16}
17
18#[utoipa::path(
20 post,
21 path = "/api/auth/register",
22 request_body = RegisterRequest,
23 responses(
24 (status = 200, description = "User registered successfully", body = TokenResponse),
25 (status = 400, description = "Invalid input"),
26 (status = 409, description = "User already exists")
27 ),
28 tag = "auth"
29)]
30pub async fn register(
31 State(state): State<AppState>,
32 Json(payload): Json<RegisterRequest>,
33) -> Result<Json<TokenResponse>> {
34 if payload.email.is_empty() || payload.password.len() < 8 {
36 return Err(AppError::InvalidInput(
37 "Email required and password must be at least 8 characters".to_string(),
38 ));
39 }
40
41 if state
43 .turso
44 .get_user_by_email(&payload.email)
45 .await?
46 .is_some()
47 {
48 return Err(AppError::InvalidInput("User already exists".to_string()));
49 }
50
51 let password_hash = state.auth_service.hash_password(&payload.password)?;
53
54 let user_id = Uuid::new_v4().to_string();
56 state
57 .turso
58 .create_user(&user_id, &payload.email, &password_hash, &payload.name)
59 .await?;
60
61 let tokens = state
63 .auth_service
64 .generate_tokens(&user_id, &payload.email)?;
65
66 let token_hash = state.auth_service.hash_token(&tokens.refresh_token);
68 let session_id = Uuid::new_v4().to_string();
69 state
70 .turso
71 .create_session(
72 &session_id,
73 &user_id,
74 &token_hash,
75 chrono::Utc::now().timestamp() + tokens.expires_in,
76 )
77 .await?;
78
79 Ok(Json(tokens))
80}
81
82#[utoipa::path(
84 post,
85 path = "/api/auth/login",
86 request_body = LoginRequest,
87 responses(
88 (status = 200, description = "Login successful", body = TokenResponse),
89 (status = 401, description = "Invalid credentials")
90 ),
91 tag = "auth"
92)]
93pub async fn login(
94 State(state): State<AppState>,
95 Json(payload): Json<LoginRequest>,
96) -> Result<Json<TokenResponse>> {
97 let user = state
99 .turso
100 .get_user_by_email(&payload.email)
101 .await?
102 .ok_or_else(|| AppError::Auth("Invalid credentials".to_string()))?;
103
104 if !state
106 .auth_service
107 .verify_password(&payload.password, &user.password_hash)?
108 {
109 return Err(AppError::Auth("Invalid credentials".to_string()));
110 }
111
112 let tokens = state.auth_service.generate_tokens(&user.id, &user.email)?;
114
115 let token_hash = state.auth_service.hash_token(&tokens.refresh_token);
117 let session_id = Uuid::new_v4().to_string();
118 state
119 .turso
120 .create_session(
121 &session_id,
122 &user.id,
123 &token_hash,
124 chrono::Utc::now().timestamp() + tokens.expires_in,
125 )
126 .await?;
127
128 Ok(Json(tokens))
129}
130
131#[derive(Debug, Deserialize, ToSchema)]
133pub struct LogoutRequest {
134 pub refresh_token: String,
136}
137
138#[derive(Debug, serde::Serialize, ToSchema)]
140pub struct LogoutResponse {
141 pub message: String,
143}
144
145#[utoipa::path(
147 post,
148 path = "/api/auth/logout",
149 request_body = LogoutRequest,
150 responses(
151 (status = 200, description = "Logout successful", body = LogoutResponse),
152 (status = 401, description = "Invalid token")
153 ),
154 tag = "auth"
155)]
156pub async fn logout(
157 State(state): State<AppState>,
158 Json(payload): Json<LogoutRequest>,
159) -> Result<Json<LogoutResponse>> {
160 let token_hash = state.auth_service.hash_token(&payload.refresh_token);
162
163 state
166 .turso
167 .delete_session_by_token_hash(&token_hash)
168 .await?;
169
170 Ok(Json(LogoutResponse {
171 message: "Logged out successfully".to_string(),
172 }))
173}
174
175#[utoipa::path(
177 post,
178 path = "/api/auth/refresh",
179 request_body = RefreshTokenRequest,
180 responses(
181 (status = 200, description = "Token refreshed successfully", body = TokenResponse),
182 (status = 401, description = "Invalid or expired refresh token")
183 ),
184 tag = "auth"
185)]
186pub async fn refresh_token(
187 State(state): State<AppState>,
188 Json(payload): Json<RefreshTokenRequest>,
189) -> Result<Json<TokenResponse>> {
190 let refresh_token = &payload.refresh_token;
191
192 let claims = state.auth_service.verify_token(refresh_token)?;
194
195 let token_hash = state.auth_service.hash_token(refresh_token);
197 let user_id = state
198 .turso
199 .validate_session(&token_hash)
200 .await?
201 .ok_or_else(|| AppError::Auth("Refresh token has been revoked or expired".to_string()))?;
202
203 if user_id != claims.sub {
205 return Err(AppError::Auth("Token mismatch".to_string()));
206 }
207
208 state
210 .turso
211 .delete_session_by_token_hash(&token_hash)
212 .await?;
213
214 let tokens = state
216 .auth_service
217 .generate_tokens(&claims.sub, &claims.email)?;
218
219 let new_token_hash = state.auth_service.hash_token(&tokens.refresh_token);
221 let session_id = Uuid::new_v4().to_string();
222 state
223 .turso
224 .create_session(
225 &session_id,
226 &claims.sub,
227 &new_token_hash,
228 chrono::Utc::now().timestamp() + tokens.expires_in,
229 )
230 .await?;
231
232 Ok(Json(tokens))
233}