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.db.get_user_by_email(&payload.email).await?.is_some() {
43 return Err(AppError::InvalidInput("User already exists".to_string()));
44 }
45
46 let password_hash = state.auth_service.hash_password(&payload.password)?;
48
49 let user_id = Uuid::new_v4().to_string();
51 state
52 .db
53 .create_user(&user_id, &payload.email, &password_hash, &payload.name)
54 .await?;
55
56 let tokens = state
58 .auth_service
59 .generate_tokens(&user_id, &payload.email)?;
60
61 let token_hash = state.auth_service.hash_token(&tokens.refresh_token);
63 let session_id = Uuid::new_v4().to_string();
64 state
65 .db
66 .create_session(
67 &session_id,
68 &user_id,
69 &token_hash,
70 chrono::Utc::now().timestamp() + tokens.expires_in,
71 )
72 .await?;
73
74 Ok(Json(tokens))
75}
76
77#[utoipa::path(
79 post,
80 path = "/api/auth/login",
81 request_body = LoginRequest,
82 responses(
83 (status = 200, description = "Login successful", body = TokenResponse),
84 (status = 401, description = "Invalid credentials")
85 ),
86 tag = "auth"
87)]
88pub async fn login(
89 State(state): State<AppState>,
90 Json(payload): Json<LoginRequest>,
91) -> Result<Json<TokenResponse>> {
92 let user = state
94 .db
95 .get_user_by_email(&payload.email)
96 .await?
97 .ok_or_else(|| AppError::Auth("Invalid credentials".to_string()))?;
98
99 if !state
101 .auth_service
102 .verify_password(&payload.password, &user.password_hash)?
103 {
104 return Err(AppError::Auth("Invalid credentials".to_string()));
105 }
106
107 let tokens = state.auth_service.generate_tokens(&user.id, &user.email)?;
109
110 let token_hash = state.auth_service.hash_token(&tokens.refresh_token);
112 let session_id = Uuid::new_v4().to_string();
113 state
114 .db
115 .create_session(
116 &session_id,
117 &user.id,
118 &token_hash,
119 chrono::Utc::now().timestamp() + tokens.expires_in,
120 )
121 .await?;
122
123 Ok(Json(tokens))
124}
125
126#[derive(Debug, Deserialize, ToSchema)]
128pub struct LogoutRequest {
129 pub refresh_token: String,
131}
132
133#[derive(Debug, serde::Serialize, ToSchema)]
135pub struct LogoutResponse {
136 pub message: String,
138}
139
140#[utoipa::path(
142 post,
143 path = "/api/auth/logout",
144 request_body = LogoutRequest,
145 responses(
146 (status = 200, description = "Logout successful", body = LogoutResponse),
147 (status = 401, description = "Invalid token")
148 ),
149 tag = "auth"
150)]
151pub async fn logout(
152 State(state): State<AppState>,
153 Json(payload): Json<LogoutRequest>,
154) -> Result<Json<LogoutResponse>> {
155 let token_hash = state.auth_service.hash_token(&payload.refresh_token);
157
158 state.db.delete_session_by_token_hash(&token_hash).await?;
161
162 Ok(Json(LogoutResponse {
163 message: "Logged out successfully".to_string(),
164 }))
165}
166
167#[utoipa::path(
169 post,
170 path = "/api/auth/refresh",
171 request_body = RefreshTokenRequest,
172 responses(
173 (status = 200, description = "Token refreshed successfully", body = TokenResponse),
174 (status = 401, description = "Invalid or expired refresh token")
175 ),
176 tag = "auth"
177)]
178pub async fn refresh_token(
179 State(state): State<AppState>,
180 Json(payload): Json<RefreshTokenRequest>,
181) -> Result<Json<TokenResponse>> {
182 let refresh_token = &payload.refresh_token;
183
184 let claims = state.auth_service.verify_token(refresh_token)?;
186
187 let token_hash = state.auth_service.hash_token(refresh_token);
189 let user_id = state
190 .db
191 .validate_session(&token_hash)
192 .await?
193 .ok_or_else(|| AppError::Auth("Refresh token has been revoked or expired".to_string()))?;
194
195 if user_id != claims.sub {
197 return Err(AppError::Auth("Token mismatch".to_string()));
198 }
199
200 state.db.delete_session_by_token_hash(&token_hash).await?;
202
203 let tokens = state
205 .auth_service
206 .generate_tokens(&claims.sub, &claims.email)?;
207
208 let new_token_hash = state.auth_service.hash_token(&tokens.refresh_token);
210 let session_id = Uuid::new_v4().to_string();
211 state
212 .db
213 .create_session(
214 &session_id,
215 &claims.sub,
216 &new_token_hash,
217 chrono::Utc::now().timestamp() + tokens.expires_in,
218 )
219 .await?;
220
221 Ok(Json(tokens))
222}