use axum::debug_handler; use loco_rs::prelude::*; use serde::{Deserialize, Serialize}; use crate::{ mailers::auth::AuthMailer, models::{ _entities::users, users::{LoginParams, RegisterParams}, }, views::auth::{CurrentResponse, LoginResponse}, }; #[derive(Debug, Deserialize, Serialize)] pub struct VerifyParams { pub token: String, } #[derive(Debug, Deserialize, Serialize)] pub struct ForgotParams { pub email: String, } #[derive(Debug, Deserialize, Serialize)] pub struct ResetParams { pub token: String, pub password: String, } /// Register function creates a new user with the given parameters and sends a /// welcome email to the user #[debug_handler] async fn register( State(ctx): State, Json(params): Json, ) -> Result { let res = users::Model::create_with_password(&ctx.db, ¶ms).await; let user = match res { Ok(user) => user, Err(err) => { tracing::info!( message = err.to_string(), user_email = ¶ms.email, "could not register user", ); return format::json(()); } }; let user = user .into_active_model() .set_email_verification_sent(&ctx.db) .await?; AuthMailer::send_welcome(&ctx, &user).await?; format::json(()) } /// Verify register user. if the user not verified his email, he can't login to /// the system. #[debug_handler] async fn verify( State(ctx): State, Json(params): Json, ) -> Result { let user = users::Model::find_by_verification_token(&ctx.db, ¶ms.token).await?; if user.email_verified_at.is_some() { tracing::info!(pid = user.pid.to_string(), "user already verified"); } else { let active_model = user.into_active_model(); let user = active_model.verified(&ctx.db).await?; tracing::info!(pid = user.pid.to_string(), "user verified"); } format::json(()) } /// In case the user forgot his password this endpoints generate a forgot token /// and send email to the user. In case the email not found in our DB, we are /// returning a valid request for for security reasons (not exposing users DB /// list). #[debug_handler] async fn forgot( State(ctx): State, Json(params): Json, ) -> Result { let Ok(user) = users::Model::find_by_email(&ctx.db, ¶ms.email).await else { // we don't want to expose our users email. if the email is invalid we still // returning success to the caller return format::json(()); }; let user = user .into_active_model() .set_forgot_password_sent(&ctx.db) .await?; AuthMailer::forgot_password(&ctx, &user).await?; format::json(()) } /// reset user password by the given parameters #[debug_handler] async fn reset(State(ctx): State, Json(params): Json) -> Result { let Ok(user) = users::Model::find_by_reset_token(&ctx.db, ¶ms.token).await else { // we don't want to expose our users email. if the email is invalid we still // returning success to the caller tracing::info!("reset token not found"); return format::json(()); }; user.into_active_model() .reset_password(&ctx.db, ¶ms.password) .await?; format::json(()) } /// Creates a user login and returns a token #[debug_handler] async fn login(State(ctx): State, Json(params): Json) -> Result { let user = users::Model::find_by_email(&ctx.db, ¶ms.email).await?; let valid = user.verify_password(¶ms.password); if !valid { return unauthorized("unauthorized!"); } let jwt_secret = ctx.config.get_jwt_config()?; let token = user .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration) .or_else(|_| unauthorized("unauthorized!"))?; format::json(LoginResponse::new(&user, &token)) } #[debug_handler] async fn current(auth: auth::JWT, State(ctx): State) -> Result { let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?; format::json(CurrentResponse::new(&user)) } pub fn routes() -> Routes { Routes::new() .prefix("/api/auth") .add("/register", post(register)) .add("/verify", post(verify)) .add("/login", post(login)) .add("/forgot", post(forgot)) .add("/reset", post(reset)) .add("/current", get(current)) }