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.db.get_user_by_email(&payload.email).await?.is_some() {
43        return Err(AppError::InvalidInput("User already exists".to_string()));
44    }
45
46    // Hash password
47    let password_hash = state.auth_service.hash_password(&payload.password)?;
48
49    // Create user
50    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    // Generate tokens
57    let tokens = state
58        .auth_service
59        .generate_tokens(&user_id, &payload.email)?;
60
61    // Store refresh token
62    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/// Login with email and password
78#[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    // Get user
93    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    // Verify password
100    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    // Generate tokens
108    let tokens = state.auth_service.generate_tokens(&user.id, &user.email)?;
109
110    // Store refresh token
111    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/// Request payload for logout
127#[derive(Debug, Deserialize, ToSchema)]
128pub struct LogoutRequest {
129    /// The refresh token to invalidate
130    pub refresh_token: String,
131}
132
133/// Response for logout
134#[derive(Debug, serde::Serialize, ToSchema)]
135pub struct LogoutResponse {
136    /// Success message
137    pub message: String,
138}
139
140/// Logout and invalidate refresh token
141#[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    // Hash the refresh token and delete the session
156    let token_hash = state.auth_service.hash_token(&payload.refresh_token);
157
158    // Attempt to delete the session - we don't error if it doesn't exist
159    // (token may already be expired/revoked, which is fine for logout)
160    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/// Refresh access token
168#[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    // Verify refresh token JWT signature and expiry
185    let claims = state.auth_service.verify_token(refresh_token)?;
186
187    // Hash the refresh token and validate it exists in the database
188    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    // Ensure the token belongs to the claimed user
196    if user_id != claims.sub {
197        return Err(AppError::Auth("Token mismatch".to_string()));
198    }
199
200    // Invalidate the old refresh token (one-time use)
201    state.db.delete_session_by_token_hash(&token_hash).await?;
202
203    // Generate new tokens
204    let tokens = state
205        .auth_service
206        .generate_tokens(&claims.sub, &claims.email)?;
207
208    // Store the new refresh token in a new session
209    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}