use crate::{
db::traits::DatabaseClient,
types::{AppError, LoginRequest, RegisterRequest, Result, TokenResponse},
AppState,
};
use axum::{extract::State, Json};
use serde::Deserialize;
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Deserialize, ToSchema)]
pub struct RefreshTokenRequest {
pub refresh_token: String,
}
#[utoipa::path(
post,
path = "/api/auth/register",
request_body = RegisterRequest,
responses(
(status = 200, description = "User registered successfully", body = TokenResponse),
(status = 400, description = "Invalid input"),
(status = 409, description = "User already exists")
),
tag = "auth"
)]
pub async fn register(
State(state): State<AppState>,
Json(payload): Json<RegisterRequest>,
) -> Result<Json<TokenResponse>> {
if payload.email.is_empty() || payload.password.len() < 8 {
return Err(AppError::InvalidInput(
"Email required and password must be at least 8 characters".to_string(),
));
}
if state
.db
.get_user_by_email(&payload.email)
.await?
.is_some()
{
return Err(AppError::InvalidInput("User already exists".to_string()));
}
let password_hash = state.auth_service.hash_password(&payload.password)?;
let user_id = Uuid::new_v4().to_string();
state
.db
.create_user(&user_id, &payload.email, &password_hash, &payload.name)
.await?;
let tokens = state
.auth_service
.generate_tokens(&user_id, &payload.email)?;
let token_hash = state.auth_service.hash_token(&tokens.refresh_token);
let session_id = Uuid::new_v4().to_string();
state
.db
.create_session(
&session_id,
&user_id,
&token_hash,
chrono::Utc::now().timestamp() + tokens.expires_in,
)
.await?;
Ok(Json(tokens))
}
#[utoipa::path(
post,
path = "/api/auth/login",
request_body = LoginRequest,
responses(
(status = 200, description = "Login successful", body = TokenResponse),
(status = 401, description = "Invalid credentials")
),
tag = "auth"
)]
pub async fn login(
State(state): State<AppState>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<TokenResponse>> {
let user = state
.db
.get_user_by_email(&payload.email)
.await?
.ok_or_else(|| AppError::Auth("Invalid credentials".to_string()))?;
if !state
.auth_service
.verify_password(&payload.password, &user.password_hash)?
{
return Err(AppError::Auth("Invalid credentials".to_string()));
}
let tokens = state.auth_service.generate_tokens(&user.id, &user.email)?;
let token_hash = state.auth_service.hash_token(&tokens.refresh_token);
let session_id = Uuid::new_v4().to_string();
state
.db
.create_session(
&session_id,
&user.id,
&token_hash,
chrono::Utc::now().timestamp() + tokens.expires_in,
)
.await?;
Ok(Json(tokens))
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct LogoutRequest {
pub refresh_token: String,
}
#[derive(Debug, serde::Serialize, ToSchema)]
pub struct LogoutResponse {
pub message: String,
}
#[utoipa::path(
post,
path = "/api/auth/logout",
request_body = LogoutRequest,
responses(
(status = 200, description = "Logout successful", body = LogoutResponse),
(status = 401, description = "Invalid token")
),
tag = "auth"
)]
pub async fn logout(
State(state): State<AppState>,
Json(payload): Json<LogoutRequest>,
) -> Result<Json<LogoutResponse>> {
let token_hash = state.auth_service.hash_token(&payload.refresh_token);
state
.db
.delete_session_by_token_hash(&token_hash)
.await?;
Ok(Json(LogoutResponse {
message: "Logged out successfully".to_string(),
}))
}
#[utoipa::path(
post,
path = "/api/auth/refresh",
request_body = RefreshTokenRequest,
responses(
(status = 200, description = "Token refreshed successfully", body = TokenResponse),
(status = 401, description = "Invalid or expired refresh token")
),
tag = "auth"
)]
pub async fn refresh_token(
State(state): State<AppState>,
Json(payload): Json<RefreshTokenRequest>,
) -> Result<Json<TokenResponse>> {
let refresh_token = &payload.refresh_token;
let claims = state.auth_service.verify_token(refresh_token)?;
let token_hash = state.auth_service.hash_token(refresh_token);
let user_id = state
.db
.validate_session(&token_hash)
.await?
.ok_or_else(|| AppError::Auth("Refresh token has been revoked or expired".to_string()))?;
if user_id != claims.sub {
return Err(AppError::Auth("Token mismatch".to_string()));
}
state
.db
.delete_session_by_token_hash(&token_hash)
.await?;
let tokens = state
.auth_service
.generate_tokens(&claims.sub, &claims.email)?;
let new_token_hash = state.auth_service.hash_token(&tokens.refresh_token);
let session_id = Uuid::new_v4().to_string();
state
.db
.create_session(
&session_id,
&claims.sub,
&new_token_hash,
chrono::Utc::now().timestamp() + tokens.expires_in,
)
.await?;
Ok(Json(tokens))
}