Skip to main content

ares/api/handlers/
auth.rs

1use 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/// Request payload for refreshing an access token
12#[derive(Debug, Deserialize, ToSchema)]
13pub struct RefreshTokenRequest {
14    /// The refresh token issued during login or registration
15    pub refresh_token: String,
16}
17
18/// Register a new user
19#[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    // Validate input
35    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    // Check if user exists
42    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    // Hash password
52    let password_hash = state.auth_service.hash_password(&payload.password)?;
53
54    // Create user
55    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    // Generate tokens
62    let tokens = state
63        .auth_service
64        .generate_tokens(&user_id, &payload.email)?;
65
66    // Store refresh token
67    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/// Login with email and password
83#[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    // Get user
98    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    // Verify password
105    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    // Generate tokens
113    let tokens = state.auth_service.generate_tokens(&user.id, &user.email)?;
114
115    // Store refresh token
116    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/// Request payload for logout
132#[derive(Debug, Deserialize, ToSchema)]
133pub struct LogoutRequest {
134    /// The refresh token to invalidate
135    pub refresh_token: String,
136}
137
138/// Response for logout
139#[derive(Debug, serde::Serialize, ToSchema)]
140pub struct LogoutResponse {
141    /// Success message
142    pub message: String,
143}
144
145/// Logout and invalidate refresh token
146#[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    // Hash the refresh token and delete the session
161    let token_hash = state.auth_service.hash_token(&payload.refresh_token);
162
163    // Attempt to delete the session - we don't error if it doesn't exist
164    // (token may already be expired/revoked, which is fine for logout)
165    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/// Refresh access token
176#[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    // Verify refresh token JWT signature and expiry
193    let claims = state.auth_service.verify_token(refresh_token)?;
194
195    // Hash the refresh token and validate it exists in the database
196    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    // Ensure the token belongs to the claimed user
204    if user_id != claims.sub {
205        return Err(AppError::Auth("Token mismatch".to_string()));
206    }
207
208    // Invalidate the old refresh token (one-time use)
209    state
210        .turso
211        .delete_session_by_token_hash(&token_hash)
212        .await?;
213
214    // Generate new tokens
215    let tokens = state
216        .auth_service
217        .generate_tokens(&claims.sub, &claims.email)?;
218
219    // Store the new refresh token in a new session
220    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}