use axum::{extract::State, http::StatusCode, Json}; use serde::Deserialize; use crate::{ error::AppResult, middleware::auth::AuthCustomer, services::{auth, mail}, state::AppState, }; #[derive(Deserialize)] pub struct RegisterRequest { pub email: String, pub password: String, #[serde(rename = "fullName")] pub full_name: String, } #[derive(Deserialize)] pub struct LoginRequest { pub email: String, pub password: String, } // POST /api/v1/auth/register pub async fn register( State(state): State, Json(body): Json, ) -> AppResult<(StatusCode, Json)> { if body.password.len() < 8 { return Err(crate::error::AppError::BadRequest("Password must be at least 8 characters".into())); } let (customer, token) = auth::register(&state.pool, &body.email, &body.password, &body.full_name).await?; // Non-fatal — log if email fails if let Err(e) = mail::send_verification(&state.config.mail, &state.config.app_url, &customer.email, &token).await { tracing::warn!("Failed to send verification email: {e}"); } Ok((StatusCode::CREATED, Json(serde_json::json!({ "message": "Account created. Check your email to verify your account.", "id": customer.id, })))) } // POST /api/v1/auth/login pub async fn login( State(state): State, Json(body): Json, ) -> AppResult> { let token = auth::login(&state.pool, &state.config, &body.email, &body.password).await?; Ok(Json(serde_json::json!({ "token": token }))) } // GET /api/v1/auth/me pub async fn me( State(state): State, AuthCustomer(claims): AuthCustomer, ) -> AppResult> { let customer = auth::get_by_id(&state.pool, &claims.sub) .await? .ok_or(crate::error::AppError::NotFound)?; Ok(Json(serde_json::to_value(&customer).unwrap())) } // GET /api/v1/auth/verify-email?token=... pub async fn verify_email( State(state): State, axum::extract::Query(params): axum::extract::Query>, ) -> AppResult> { let token = params.get("token").ok_or_else(|| crate::error::AppError::BadRequest("Missing token".into()))?; let ok = auth::verify_email(&state.pool, token).await?; if !ok { return Err(crate::error::AppError::BadRequest("Invalid or expired verification link".into())); } Ok(Json(serde_json::json!({ "message": "Email verified. You can now log in." }))) } // POST /api/v1/auth/forgot-password pub async fn forgot_password( State(state): State, Json(body): Json, ) -> AppResult> { if let Some(email) = body["email"].as_str() { if let Ok(Some(token)) = auth::initiate_reset(&state.pool, email).await { let _ = mail::send_password_reset(&state.config.mail, &state.config.app_url, email, &token).await; } } // Always return success to prevent email enumeration Ok(Json(serde_json::json!({ "message": "If that email exists, a reset link has been sent." }))) } // POST /api/v1/auth/reset-password pub async fn reset_password( State(state): State, Json(body): Json, ) -> AppResult> { let token = body["token"].as_str().ok_or_else(|| crate::error::AppError::BadRequest("Missing token".into()))?; let password = body["password"].as_str().ok_or_else(|| crate::error::AppError::BadRequest("Missing password".into()))?; if password.len() < 8 { return Err(crate::error::AppError::BadRequest("Password must be at least 8 characters".into())); } let ok = auth::reset_password(&state.pool, token, password).await?; if !ok { return Err(crate::error::AppError::BadRequest("Invalid or expired reset token".into())); } Ok(Json(serde_json::json!({ "message": "Password updated. You can now log in." }))) }