feat: rewrite website API in Rust — axum, sqlx, Stripe, lettre, argon2, MariaDB
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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<AppState>,
|
||||
Json(body): Json<RegisterRequest>,
|
||||
) -> AppResult<(StatusCode, Json<serde_json::Value>)> {
|
||||
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<AppState>,
|
||||
Json(body): Json<LoginRequest>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
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<AppState>,
|
||||
AuthCustomer(claims): AuthCustomer,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
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<AppState>,
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
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<AppState>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
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<AppState>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
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." })))
|
||||
}
|
||||
Reference in New Issue
Block a user