Files
nixpanel-website/api/src/routes/auth.rs
T

112 lines
4.0 KiB
Rust

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." })))
}