From fff675b9c3532acf8368548be2d26f6d276faec8 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 19 May 2026 14:30:54 +0800 Subject: [PATCH 1/4] feat: add login first connection --- docker-compose.yml | 6 +- src/database/auth/change_password/mod.rs | 5 + src/database/auth/change_password/query.rs | 17 +++ src/database/auth/change_password/view.rs | 69 ++++++++++ src/database/auth/is_first_time/mod.rs | 5 + src/database/auth/is_first_time/query.rs | 16 +++ src/database/auth/is_first_time/view.rs | 28 ++++ src/database/auth/login/mod.rs | 5 +- src/database/auth/login/result_view.rs | 21 --- src/database/auth/login/view.rs | 34 ++++- src/database/auth/mod.rs | 3 + .../auth/unset_first_connection/mod.rs | 5 + .../auth/unset_first_connection/query.rs | 16 +++ .../auth/unset_first_connection/view.rs | 40 ++++++ src/endpoints/v1/auth/doc.rs | 6 + .../v1/auth/force_change_password/doc.rs | 9 ++ .../v1/auth/force_change_password/endpoint.rs | 123 ++++++++++++++++++ .../v1/auth/force_change_password/mod.rs | 3 + .../v1/auth/force_change_password/view.rs | 18 +++ src/endpoints/v1/auth/forgot_password/doc.rs | 9 ++ .../v1/auth/forgot_password/endpoint.rs | 86 ++++++++++++ src/endpoints/v1/auth/forgot_password/mod.rs | 3 + src/endpoints/v1/auth/forgot_password/view.rs | 13 ++ src/endpoints/v1/auth/login/doc.rs | 2 +- src/endpoints/v1/auth/login/endpoint.rs | 104 ++++++++++++--- .../v1/auth/login/login_response_view.rs | 36 ----- src/endpoints/v1/auth/login/login_view.rs | 34 ----- src/endpoints/v1/auth/login/mod.rs | 3 +- src/endpoints/v1/auth/login/view.rs | 94 +++++++++++++ src/endpoints/v1/auth/mod.rs | 8 +- src/endpoints/v1/auth/reset_password/doc.rs | 9 ++ .../v1/auth/reset_password/endpoint.rs | 113 ++++++++++++++++ src/endpoints/v1/auth/reset_password/mod.rs | 3 + src/endpoints/v1/auth/reset_password/view.rs | 57 ++++++++ tests/queries/auth/login.rs | 2 +- 35 files changed, 882 insertions(+), 123 deletions(-) create mode 100644 src/database/auth/change_password/mod.rs create mode 100644 src/database/auth/change_password/query.rs create mode 100644 src/database/auth/change_password/view.rs create mode 100644 src/database/auth/is_first_time/mod.rs create mode 100644 src/database/auth/is_first_time/query.rs create mode 100644 src/database/auth/is_first_time/view.rs delete mode 100644 src/database/auth/login/result_view.rs create mode 100644 src/database/auth/unset_first_connection/mod.rs create mode 100644 src/database/auth/unset_first_connection/query.rs create mode 100644 src/database/auth/unset_first_connection/view.rs create mode 100644 src/endpoints/v1/auth/force_change_password/doc.rs create mode 100644 src/endpoints/v1/auth/force_change_password/endpoint.rs create mode 100644 src/endpoints/v1/auth/force_change_password/mod.rs create mode 100644 src/endpoints/v1/auth/force_change_password/view.rs create mode 100644 src/endpoints/v1/auth/forgot_password/doc.rs create mode 100644 src/endpoints/v1/auth/forgot_password/endpoint.rs create mode 100644 src/endpoints/v1/auth/forgot_password/mod.rs create mode 100644 src/endpoints/v1/auth/forgot_password/view.rs delete mode 100644 src/endpoints/v1/auth/login/login_response_view.rs delete mode 100644 src/endpoints/v1/auth/login/login_view.rs create mode 100644 src/endpoints/v1/auth/login/view.rs create mode 100644 src/endpoints/v1/auth/reset_password/doc.rs create mode 100644 src/endpoints/v1/auth/reset_password/endpoint.rs create mode 100644 src/endpoints/v1/auth/reset_password/mod.rs create mode 100644 src/endpoints/v1/auth/reset_password/view.rs diff --git a/docker-compose.yml b/docker-compose.yml index 401bbd4..6882358 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ x-service: &service-template services: postgres: - image: ghcr.io/mairie360/database:dev-b816587 + image: ghcr.io/mairie360/database:dev-e9990d6 restart: always container_name: mairie360-db-core-api environment: @@ -62,7 +62,7 @@ services: retries: 5 liquibase: - image: ghcr.io/mairie360/liquibase-migrations:dev-b816587 + image: ghcr.io/mairie360/liquibase-migrations:dev-e9990d6 container_name: mairie360-liquibase-core-api depends_on: postgres: @@ -148,4 +148,4 @@ services: - backend depends_on: core: - condition: service_healthy \ No newline at end of file + condition: service_healthy diff --git a/src/database/auth/change_password/mod.rs b/src/database/auth/change_password/mod.rs new file mode 100644 index 0000000..8d95cfe --- /dev/null +++ b/src/database/auth/change_password/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::login_query; + +mod view; +pub use view::{LoginUserQueryResultView, LoginUserQueryView}; diff --git a/src/database/auth/change_password/query.rs b/src/database/auth/change_password/query.rs new file mode 100644 index 0000000..2ed6718 --- /dev/null +++ b/src/database/auth/change_password/query.rs @@ -0,0 +1,17 @@ +use crate::database::auth::login::LoginUserQueryResultView; +use crate::database::auth::login::LoginUserQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn login_query( + view: LoginUserQueryView, + pool: PgPool, +) -> Result, DatabaseError> { + let result = sqlx::query_as::<_, LoginUserQueryResultView>(&view.get_request()) + .bind(view.get_email()) + .fetch_optional(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/auth/change_password/view.rs b/src/database/auth/change_password/view.rs new file mode 100644 index 0000000..420d937 --- /dev/null +++ b/src/database/auth/change_password/view.rs @@ -0,0 +1,69 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct LoginUserQueryView { + email: String, + password: String, +} + +impl LoginUserQueryView { + pub fn new(email: String, password: String) -> Self { + Self { email, password } + } + + pub fn get_email(&self) -> &String { + &self.email + } + + pub fn get_password(&self) -> &String { + &self.password + } +} + +impl DatabaseQueryView for LoginUserQueryView { + fn get_request(&self) -> String { + "SELECT id, password, first_connect FROM users WHERE email = $1".to_string() + } +} + +impl Display for LoginUserQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LoginUserQueryView: email = {}, password = [PROTECTED]", + self.email + ) + } +} + +#[derive(Debug, sqlx::FromRow, PartialEq, Eq)] +pub struct LoginUserQueryResultView { + #[sqlx(rename = "id")] + user_id: i32, + #[sqlx(rename = "password")] + password: String, + #[sqlx(rename = "first_connect")] + first_connect: bool, +} + +impl LoginUserQueryResultView { + pub fn new(user_id: i32, password: String, first_connect: bool) -> Self { + Self { + user_id, + password, + first_connect, + } + } + + pub fn password(&self) -> &str { + &self.password + } + + pub fn user_id(&self) -> i32 { + self.user_id + } + + pub fn first_connect(&self) -> bool { + self.first_connect + } +} diff --git a/src/database/auth/is_first_time/mod.rs b/src/database/auth/is_first_time/mod.rs new file mode 100644 index 0000000..57c67b6 --- /dev/null +++ b/src/database/auth/is_first_time/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::is_first_time_query; + +mod view; +pub use view::IsFirstTimeQueryView; diff --git a/src/database/auth/is_first_time/query.rs b/src/database/auth/is_first_time/query.rs new file mode 100644 index 0000000..d041c7f --- /dev/null +++ b/src/database/auth/is_first_time/query.rs @@ -0,0 +1,16 @@ +use crate::database::auth::is_first_time::IsFirstTimeQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn is_first_time_query( + view: IsFirstTimeQueryView, + pool: PgPool, +) -> Result { + let result = sqlx::query_scalar::<_, bool>(&view.get_request()) + .bind(view.user_id() as i32) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/auth/is_first_time/view.rs b/src/database/auth/is_first_time/view.rs new file mode 100644 index 0000000..7f09256 --- /dev/null +++ b/src/database/auth/is_first_time/view.rs @@ -0,0 +1,28 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct IsFirstTimeQueryView { + user_id: u64, +} + +impl IsFirstTimeQueryView { + pub fn new(user_id: u64) -> Self { + Self { user_id } + } + + pub fn user_id(&self) -> u64 { + self.user_id + } +} + +impl DatabaseQueryView for IsFirstTimeQueryView { + fn get_request(&self) -> String { + "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1) AS first_connect".to_string() + } +} + +impl Display for IsFirstTimeQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "IsFirstTimeQueryView: user_id = {}", self.user_id) + } +} diff --git a/src/database/auth/login/mod.rs b/src/database/auth/login/mod.rs index 1157591..8d95cfe 100644 --- a/src/database/auth/login/mod.rs +++ b/src/database/auth/login/mod.rs @@ -2,7 +2,4 @@ mod query; pub use query::login_query; mod view; -pub use view::LoginUserQueryView; - -mod result_view; -pub use result_view::LoginUserQueryResultView; +pub use view::{LoginUserQueryResultView, LoginUserQueryView}; diff --git a/src/database/auth/login/result_view.rs b/src/database/auth/login/result_view.rs deleted file mode 100644 index e2b9bf5..0000000 --- a/src/database/auth/login/result_view.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[derive(Debug, sqlx::FromRow, PartialEq, Eq)] -pub struct LoginUserQueryResultView { - #[sqlx(rename = "id")] - user_id: i32, - #[sqlx(rename = "password")] - password: String, -} - -impl LoginUserQueryResultView { - pub fn new(user_id: i32, password: String) -> Self { - Self { user_id, password } - } - - pub fn password(&self) -> &str { - &self.password - } - - pub fn user_id(&self) -> i32 { - self.user_id - } -} diff --git a/src/database/auth/login/view.rs b/src/database/auth/login/view.rs index 7e91504..420d937 100644 --- a/src/database/auth/login/view.rs +++ b/src/database/auth/login/view.rs @@ -22,7 +22,7 @@ impl LoginUserQueryView { impl DatabaseQueryView for LoginUserQueryView { fn get_request(&self) -> String { - "SELECT id, password FROM users WHERE email = $1".to_string() + "SELECT id, password, first_connect FROM users WHERE email = $1".to_string() } } @@ -35,3 +35,35 @@ impl Display for LoginUserQueryView { ) } } + +#[derive(Debug, sqlx::FromRow, PartialEq, Eq)] +pub struct LoginUserQueryResultView { + #[sqlx(rename = "id")] + user_id: i32, + #[sqlx(rename = "password")] + password: String, + #[sqlx(rename = "first_connect")] + first_connect: bool, +} + +impl LoginUserQueryResultView { + pub fn new(user_id: i32, password: String, first_connect: bool) -> Self { + Self { + user_id, + password, + first_connect, + } + } + + pub fn password(&self) -> &str { + &self.password + } + + pub fn user_id(&self) -> i32 { + self.user_id + } + + pub fn first_connect(&self) -> bool { + self.first_connect + } +} diff --git a/src/database/auth/mod.rs b/src/database/auth/mod.rs index 8d32f68..8956e98 100644 --- a/src/database/auth/mod.rs +++ b/src/database/auth/mod.rs @@ -1,2 +1,5 @@ +pub mod change_password; +pub mod is_first_time; pub mod login; pub mod register; +pub mod unset_first_connection; diff --git a/src/database/auth/unset_first_connection/mod.rs b/src/database/auth/unset_first_connection/mod.rs new file mode 100644 index 0000000..415703c --- /dev/null +++ b/src/database/auth/unset_first_connection/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::unset_first_connection_query; + +mod view; +pub use view::UnsetFirstConnectionQueryView; diff --git a/src/database/auth/unset_first_connection/query.rs b/src/database/auth/unset_first_connection/query.rs new file mode 100644 index 0000000..5c8db0f --- /dev/null +++ b/src/database/auth/unset_first_connection/query.rs @@ -0,0 +1,16 @@ +use crate::database::auth::unset_first_connection::UnsetFirstConnectionQueryView; +use mairie360_api_lib::database::{db_interface::DatabaseQueryView, errors::DatabaseError}; +use sqlx::PgPool; + +pub async fn unset_first_connection_query( + view: UnsetFirstConnectionQueryView, + pool: PgPool, +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.password()) + .bind(view.user_id() as i32) + .execute(&pool) + .await?; + + Ok(()) +} diff --git a/src/database/auth/unset_first_connection/view.rs b/src/database/auth/unset_first_connection/view.rs new file mode 100644 index 0000000..f2d48da --- /dev/null +++ b/src/database/auth/unset_first_connection/view.rs @@ -0,0 +1,40 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct UnsetFirstConnectionQueryView { + user_id: u64, + password: String, +} + +impl UnsetFirstConnectionQueryView { + pub fn new(user_id: u64, password: &str) -> Self { + Self { + user_id, + password: password.to_string(), + } + } + + pub fn user_id(&self) -> u64 { + self.user_id + } + + pub fn password(&self) -> &str { + &self.password + } +} + +impl DatabaseQueryView for UnsetFirstConnectionQueryView { + fn get_request(&self) -> String { + "UPDATE users SET first_connect = false AND password = $1 WHERE id = $2".to_string() + } +} + +impl Display for UnsetFirstConnectionQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "UnsetFirstConnectionQueryView: user_id = {}, password = [PROTECTED]", + self.user_id + ) + } +} diff --git a/src/endpoints/v1/auth/doc.rs b/src/endpoints/v1/auth/doc.rs index f8498ab..0280ac4 100644 --- a/src/endpoints/v1/auth/doc.rs +++ b/src/endpoints/v1/auth/doc.rs @@ -1,10 +1,16 @@ +use crate::endpoints::v1::auth::force_change_password::doc::ForceChangePasswordDoc; +use crate::endpoints::v1::auth::forgot_password::doc::ForgotPasswordDoc; use crate::endpoints::v1::auth::login::doc::LoginDoc; use crate::endpoints::v1::auth::register::doc::RegisterDoc; +use crate::endpoints::v1::auth::reset_password::doc::ResetPasswordDoc; use utoipa::OpenApi; #[derive(OpenApi)] #[openapi(nest( + (path = "/force-change-password", api = ForceChangePasswordDoc, tags = ["Authentication"]), + (path = "/forgot-password", api = ForgotPasswordDoc, tags = ["Authentication"]), (path = "/register", api = RegisterDoc, tags = ["Authentication"]), (path = "/login", api = LoginDoc, tags = ["Authentication"]), + (path = "/reset-password", api = ResetPasswordDoc, tags = ["Authentication"]), ))] pub struct AuthDoc; diff --git a/src/endpoints/v1/auth/force_change_password/doc.rs b/src/endpoints/v1/auth/force_change_password/doc.rs new file mode 100644 index 0000000..b439b9c --- /dev/null +++ b/src/endpoints/v1/auth/force_change_password/doc.rs @@ -0,0 +1,9 @@ +use crate::endpoints::v1::auth::force_change_password::endpoint; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths(endpoint::force_change_password), + components(schemas(super::view::ForceChangePasswordView)) +)] +pub struct ForceChangePasswordDoc; diff --git a/src/endpoints/v1/auth/force_change_password/endpoint.rs b/src/endpoints/v1/auth/force_change_password/endpoint.rs new file mode 100644 index 0000000..0e2f536 --- /dev/null +++ b/src/endpoints/v1/auth/force_change_password/endpoint.rs @@ -0,0 +1,123 @@ +use crate::database::auth::is_first_time::{is_first_time_query, IsFirstTimeQueryView}; +use crate::database::auth::unset_first_connection::{ + unset_first_connection_query, UnsetFirstConnectionQueryView, +}; +use crate::endpoints::v1::auth::force_change_password::view::ForceChangePasswordView; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::redis::simple_key::secured::handle_secure_get; +use mairie360_api_lib::pool::AppState; +use sqlx::PgPool; + +#[derive(Debug, Clone, PartialEq)] +enum ForceChanhePasswordError { + DatabaseError, + Forbidden, + Unauthorized, +} + +impl std::fmt::Display for ForceChanhePasswordError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ForceChanhePasswordError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + ForceChanhePasswordError::Forbidden => { + write!(f, "Unknown user token") + } + ForceChanhePasswordError::Unauthorized => { + write!(f, "Unauthorized") + } + } + } +} + +impl ResponseError for ForceChanhePasswordError { + fn status_code(&self) -> StatusCode { + match self { + ForceChanhePasswordError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + ForceChanhePasswordError::Forbidden => StatusCode::FORBIDDEN, + ForceChanhePasswordError::Unauthorized => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_user_id(state: &AppState, token: &str) -> Option { + println!("{}", format!("{}/first_connection_id", token)); + match handle_secure_get( + state.get_redis_conn().await.unwrap(), + &format!("{}/first_connection_id", token), + ) + .await + { + Ok(id) => Some(id.parse().unwrap()), + Err(_) => None, + } +} + +async fn is_first_time(pool: PgPool, user_id: u64) -> bool { + is_first_time_query(IsFirstTimeQueryView::new(user_id), pool) + .await + .unwrap_or(false) +} + +async fn change_password( + pool: PgPool, + user_id: u64, + new_password: &str, +) -> Result<(), ForceChanhePasswordError> { + unset_first_connection_query( + UnsetFirstConnectionQueryView::new(user_id, new_password), + pool, + ) + .await + .map_err(|_| ForceChanhePasswordError::DatabaseError) +} + +async fn force_change_password_trigger( + state: web::Data, + view: ForceChangePasswordView, +) -> Result<(), ForceChanhePasswordError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(ForceChanhePasswordError::DatabaseError), + }; + + let user_id = match get_user_id(&state, view.token()).await { + Some(user_id) => user_id, + None => return Err(ForceChanhePasswordError::Forbidden), + }; + + if !is_first_time(pool.clone(), user_id).await { + return Err(ForceChanhePasswordError::Unauthorized); + } + + change_password(pool.clone(), user_id, view.new_password()).await?; + + Ok(()) +} + +#[utoipa::path( + post, + path = "/", + responses( + (status = 200, description = "Password changed successfully"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Unknown user token"), + (status = 500, description = "Internal server error") + ), + tag = "Auth" +)] +#[post("/force_change_password")] +pub async fn force_change_password( + state: web::Data, + body: web::Json, +) -> Result { + force_change_password_trigger(state, body.into_inner()).await?; + Ok(HttpResponse::Ok()) +} diff --git a/src/endpoints/v1/auth/force_change_password/mod.rs b/src/endpoints/v1/auth/force_change_password/mod.rs new file mode 100644 index 0000000..b74ee2f --- /dev/null +++ b/src/endpoints/v1/auth/force_change_password/mod.rs @@ -0,0 +1,3 @@ +pub mod doc; +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/auth/force_change_password/view.rs b/src/endpoints/v1/auth/force_change_password/view.rs new file mode 100644 index 0000000..636b859 --- /dev/null +++ b/src/endpoints/v1/auth/force_change_password/view.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ForceChangePasswordView { + token: String, + new_password: String, +} + +impl ForceChangePasswordView { + pub fn token(&self) -> &str { + &self.token + } + + pub fn new_password(&self) -> &str { + &self.new_password + } +} diff --git a/src/endpoints/v1/auth/forgot_password/doc.rs b/src/endpoints/v1/auth/forgot_password/doc.rs new file mode 100644 index 0000000..b564a4e --- /dev/null +++ b/src/endpoints/v1/auth/forgot_password/doc.rs @@ -0,0 +1,9 @@ +use crate::endpoints::v1::auth::forgot_password::endpoint; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths(endpoint::forgot_password), + components(schemas(super::view::ForgotPasswordView)) +)] +pub struct ForgotPasswordDoc; diff --git a/src/endpoints/v1/auth/forgot_password/endpoint.rs b/src/endpoints/v1/auth/forgot_password/endpoint.rs new file mode 100644 index 0000000..c9dc973 --- /dev/null +++ b/src/endpoints/v1/auth/forgot_password/endpoint.rs @@ -0,0 +1,86 @@ +use crate::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; +use crate::endpoints::v1::auth::forgot_password::view::ForgotPasswordView; +use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; + +#[derive(Debug, Clone, PartialEq)] +enum ResetPasswordError { + DatabaseError, + UserNotFound, +} + +impl std::fmt::Display for ResetPasswordError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResetPasswordError::DatabaseError => { + write!(f, "An error occurred while accessing the database.") + } + ResetPasswordError::UserNotFound => { + write!(f, "User not found.") + } + } + } +} + +impl ResponseError for ResetPasswordError { + fn status_code(&self) -> StatusCode { + match self { + ResetPasswordError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::UserNotFound => StatusCode::NOT_FOUND, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn check_user(pool: &sqlx::Pool, email: &str) -> bool { + todo!() +} + +async fn trigger(pool: &sqlx::Pool, email: &str) -> Result<(), ResetPasswordError> { + todo!() +} + +async fn forgot_password_trigger( + state: web::Data, + view: ForgotPasswordView, +) -> Result<(), ResetPasswordError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(ResetPasswordError::DatabaseError), + }; + + if !check_user(&pool, view.email()).await { + return Err(ResetPasswordError::UserNotFound); + } + + trigger(&pool, view.email()).await?; + + Ok(()) +} + +#[utoipa::path( + get, + path = "/", + responses( + (status = 200, description = "Forgot password request sent successfully"), + (status = 400, description = "Bad request"), + (status = 404, description = "User not found"), + (status = 500, description = "Internal server error") + ), + tag = "Auth", + security( + ("jwt" = []) + ) +)] +#[get("/forgot_password")] +pub async fn forgot_password( + state: web::Data, + body: web::Json, +) -> Result { + forgot_password_trigger(state, body.into_inner()).await?; + Ok(HttpResponse::Ok()) +} diff --git a/src/endpoints/v1/auth/forgot_password/mod.rs b/src/endpoints/v1/auth/forgot_password/mod.rs new file mode 100644 index 0000000..b74ee2f --- /dev/null +++ b/src/endpoints/v1/auth/forgot_password/mod.rs @@ -0,0 +1,3 @@ +pub mod doc; +pub mod endpoint; +pub mod view; diff --git a/src/endpoints/v1/auth/forgot_password/view.rs b/src/endpoints/v1/auth/forgot_password/view.rs new file mode 100644 index 0000000..8cd999e --- /dev/null +++ b/src/endpoints/v1/auth/forgot_password/view.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ForgotPasswordView { + email: String, +} + +impl ForgotPasswordView { + pub fn email(&self) -> &str { + &self.email + } +} diff --git a/src/endpoints/v1/auth/login/doc.rs b/src/endpoints/v1/auth/login/doc.rs index 70bc929..8f71cc2 100644 --- a/src/endpoints/v1/auth/login/doc.rs +++ b/src/endpoints/v1/auth/login/doc.rs @@ -4,6 +4,6 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( paths(endpoint::login), - components(schemas(super::login_view::LoginView)) + components(schemas(super::view::LoginView, super::view::LoginResponseView)) )] pub struct LoginDoc; diff --git a/src/endpoints/v1/auth/login/endpoint.rs b/src/endpoints/v1/auth/login/endpoint.rs index 6f85402..555a2ef 100644 --- a/src/endpoints/v1/auth/login/endpoint.rs +++ b/src/endpoints/v1/auth/login/endpoint.rs @@ -1,21 +1,24 @@ +use super::view::{LoginResponseView, LoginView}; use crate::database::auth::login::{login_query, LoginUserQueryView}; use crate::database::sessions::create_session::CreateSessionQueryView; use crate::endpoints::v1::auth::create_new_session; +use crate::endpoints::v1::auth::login::view::LoginFirstConnectionResponseView; use actix_web::{ dev::ConnectionInfo, http::StatusCode, post, web, HttpResponse, Responder, ResponseError, }; use base64::{engine::general_purpose, Engine as _}; -use mairie360_api_lib::pool::AppState; -use rand::{rng, RngCore}; - -use super::login_response_view::LoginResponseView; -use super::login_view::LoginView; use mairie360_api_lib::jwt_manager::generate_jwt; +use mairie360_api_lib::pool::redis::simple_key::secured::{handle_secure_get, handle_secure_post}; +use mairie360_api_lib::pool::AppState; +use rand::{rng, Rng}; +use uuid::Uuid; #[derive(Debug, Clone, PartialEq)] -enum LoginError { - InvalidCredentials, +pub enum LoginError { DatabaseError, + FirstConnectError(String), + InvalidCredentials, + RedisError, TokenGenerationError, } @@ -27,6 +30,10 @@ impl std::fmt::Display for LoginError { write!(f, "An error occurred while accessing the database.") } LoginError::TokenGenerationError => write!(f, "Failed to generate JWT token."), + LoginError::FirstConnectError(token) => { + write!(f, "{}", token.to_string()) + } + LoginError::RedisError => write!(f, "Internal Redis error."), } } } @@ -34,13 +41,19 @@ impl std::fmt::Display for LoginError { impl ResponseError for LoginError { fn status_code(&self) -> StatusCode { match self { - LoginError::InvalidCredentials => StatusCode::UNAUTHORIZED, LoginError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + LoginError::FirstConnectError(_) => StatusCode::PRECONDITION_FAILED, + LoginError::InvalidCredentials => StatusCode::UNAUTHORIZED, + LoginError::RedisError => StatusCode::INTERNAL_SERVER_ERROR, LoginError::TokenGenerationError => StatusCode::INTERNAL_SERVER_ERROR, } } fn error_response(&self) -> HttpResponse { + if self.status_code() == StatusCode::PRECONDITION_FAILED { + return HttpResponse::build(self.status_code()) + .json(LoginFirstConnectionResponseView::new(self.to_string())); + } HttpResponse::build(self.status_code()).body(self.to_string()) } } @@ -56,6 +69,61 @@ fn generate_refresh_token() -> String { general_purpose::URL_SAFE_NO_PAD.encode(buffer) } +pub async fn generate_session( + user_id: u64, + device_info: &str, + ip_adress: std::net::IpAddr, + state: web::Data, +) -> Result<(String, String), LoginError> { + let refresh_token = generate_refresh_token(); + let view = CreateSessionQueryView::new(user_id, &refresh_token, device_info, ip_adress); + create_new_session(state, user_id, view).await; + let jwt = generate_jwt(user_id.to_string().as_str()).map_err(|e| { + eprintln!("JWT Generation Error: {}", e); + LoginError::TokenGenerationError + })?; + Ok((jwt, refresh_token)) +} + +async fn generate_first_connection_token( + user_id: u64, + state: web::Data, +) -> Result { + match handle_secure_get( + state.get_redis_conn().await.unwrap(), + &format!("{}/first_connection_token", user_id), + ) + .await + { + Ok(token) => return Ok(token), + _ => {} + }; + let token = Uuid::new_v4().to_string(); + println!("{}", format!("{}/first_connection_id", token)); + println!("{}", &format!("{}/first_connection_token", user_id)); + handle_secure_post( + state.get_redis_conn().await.unwrap(), + &format!("{}/first_connection_token", user_id), + &token, + ) + .await + .map_err(|e| { + eprintln!("Redis Error: {}", e); + LoginError::RedisError + })?; + handle_secure_post( + state.get_redis_conn().await.unwrap(), + &format!("{}/first_connection_id", token), + &format!("{}", user_id), + ) + .await + .map_err(|e| { + eprintln!("Redis Error: {}", e); + LoginError::RedisError + })?; + Ok(token) +} + async fn login_user( login_view: &LoginView, state: web::Data, @@ -71,20 +139,17 @@ async fn login_user( })?; match user_record { + Some(user) if user.first_connect() => Err(LoginError::FirstConnectError( + generate_first_connection_token(user.user_id() as u64, state).await?, + )), Some(user) if login_view.password() == user.password().trim() => { - let refresh_token = generate_refresh_token(); - let view = CreateSessionQueryView::new( + generate_session( user.user_id() as u64, - &refresh_token, &login_view.device_info(), ip_adress, - ); - create_new_session(state.clone(), user.user_id() as u64, view).await; - let jwt = generate_jwt(user.user_id().to_string().as_str()).map_err(|e| { - eprintln!("JWT Generation Error: {}", e); - LoginError::TokenGenerationError - })?; - Ok((jwt, refresh_token)) + state, + ) + .await } _ => { eprintln!( @@ -102,7 +167,8 @@ async fn login_user( request_body = LoginView, responses( (status = 200, description = "User login successfully!", body = LoginResponseView), - (status = 401, description = "Invalid credentials provided."), + (status = 401, description = "Invalid credentials provided.", body = LoginFirstConnectionResponseView), + (status = 412, description = "User needs to change password because first login"), (status = 500, description = "Internal server error") ), tag = "Authentication" diff --git a/src/endpoints/v1/auth/login/login_response_view.rs b/src/endpoints/v1/auth/login/login_response_view.rs deleted file mode 100644 index ab0d95a..0000000 --- a/src/endpoints/v1/auth/login/login_response_view.rs +++ /dev/null @@ -1,36 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use utoipa::ToSchema; - -#[derive(Serialize, Deserialize, ToSchema)] -pub struct LoginResponseView { - refresh_token: String, -} - -impl LoginResponseView { - pub fn new(refresh_token: String) -> Self { - LoginResponseView { refresh_token } - } - - pub fn refresh_token(&self) -> &str { - &self.refresh_token - } -} - -impl Display for LoginResponseView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "LoginResponseView {{ refresh_token: {} }}", - self.refresh_token - ) - } -} - -impl From for LoginResponseView { - fn from(token: String) -> Self { - LoginResponseView { - refresh_token: token, - } - } -} diff --git a/src/endpoints/v1/auth/login/login_view.rs b/src/endpoints/v1/auth/login/login_view.rs deleted file mode 100644 index 8aa1ebd..0000000 --- a/src/endpoints/v1/auth/login/login_view.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use utoipa::ToSchema; - -#[derive(Serialize, Deserialize, ToSchema)] -pub struct LoginView { - email: String, - password: String, - device_info: String, -} - -impl LoginView { - pub fn email(&self) -> String { - self.email.clone() - } - - pub fn password(&self) -> String { - self.password.clone() - } - - pub fn device_info(&self) -> String { - self.device_info.clone() - } -} - -impl Display for LoginView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "LoginView {{ email: {}, password: {}, device_info: {} }}", - self.email, self.password, self.device_info - ) - } -} diff --git a/src/endpoints/v1/auth/login/mod.rs b/src/endpoints/v1/auth/login/mod.rs index aa96c80..b74ee2f 100644 --- a/src/endpoints/v1/auth/login/mod.rs +++ b/src/endpoints/v1/auth/login/mod.rs @@ -1,4 +1,3 @@ pub mod doc; pub mod endpoint; -pub mod login_response_view; -pub mod login_view; +pub mod view; diff --git a/src/endpoints/v1/auth/login/view.rs b/src/endpoints/v1/auth/login/view.rs new file mode 100644 index 0000000..7f50fb3 --- /dev/null +++ b/src/endpoints/v1/auth/login/view.rs @@ -0,0 +1,94 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct LoginView { + email: String, + password: String, + device_info: String, +} + +impl LoginView { + pub fn email(&self) -> String { + self.email.clone() + } + + pub fn password(&self) -> String { + self.password.clone() + } + + pub fn device_info(&self) -> String { + self.device_info.clone() + } +} + +impl Display for LoginView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LoginView {{ email: {}, password: {}, device_info: {} }}", + self.email, self.password, self.device_info + ) + } +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct LoginResponseView { + refresh_token: String, +} + +impl LoginResponseView { + pub fn new(refresh_token: String) -> Self { + LoginResponseView { refresh_token } + } + + pub fn refresh_token(&self) -> &str { + &self.refresh_token + } +} + +impl Display for LoginResponseView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LoginResponseView {{ refresh_token: {} }}", + self.refresh_token + ) + } +} + +impl From for LoginResponseView { + fn from(token: String) -> Self { + LoginResponseView { + refresh_token: token, + } + } +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct LoginFirstConnectionResponseView { + token: String, +} + +impl LoginFirstConnectionResponseView { + pub fn new(token: String) -> Self { + LoginFirstConnectionResponseView { token } + } + + pub fn token(&self) -> &str { + &self.token + } +} + +impl Display for LoginFirstConnectionResponseView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ \"token\": {} }}", self.token) + } +} + +impl From for LoginFirstConnectionResponseView { + fn from(token: String) -> Self { + LoginFirstConnectionResponseView { token } + } +} diff --git a/src/endpoints/v1/auth/mod.rs b/src/endpoints/v1/auth/mod.rs index 50cd355..5e1169d 100644 --- a/src/endpoints/v1/auth/mod.rs +++ b/src/endpoints/v1/auth/mod.rs @@ -1,6 +1,9 @@ pub mod doc; +pub mod force_change_password; +pub mod forgot_password; pub mod login; pub mod register; +pub mod reset_password; use actix_web::web; use mairie360_api_lib::pool::AppState; @@ -13,8 +16,11 @@ use crate::database::sessions::{ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/auth") + .service(force_change_password::endpoint::force_change_password) + .service(forgot_password::endpoint::forgot_password) .service(login::endpoint::login) - .service(register::endpoint::register), + .service(register::endpoint::register) + .service(reset_password::endpoint::reset_password), ); } diff --git a/src/endpoints/v1/auth/reset_password/doc.rs b/src/endpoints/v1/auth/reset_password/doc.rs new file mode 100644 index 0000000..f0ced3d --- /dev/null +++ b/src/endpoints/v1/auth/reset_password/doc.rs @@ -0,0 +1,9 @@ +use crate::endpoints::v1::auth::reset_password::endpoint; +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths(endpoint::reset_password), + components(schemas(super::view::ResetPasswordView)) +)] +pub struct ResetPasswordDoc; diff --git a/src/endpoints/v1/auth/reset_password/endpoint.rs b/src/endpoints/v1/auth/reset_password/endpoint.rs new file mode 100644 index 0000000..34faf20 --- /dev/null +++ b/src/endpoints/v1/auth/reset_password/endpoint.rs @@ -0,0 +1,113 @@ +use crate::endpoints::v1::auth::login::endpoint::generate_session; +use crate::endpoints::v1::auth::reset_password::view::{ + ResetPasswordResponseView, ResetPasswordView, +}; +use actix_web::dev::ConnectionInfo; +use actix_web::http::StatusCode; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::AppState; + +#[derive(Debug, Clone, PartialEq)] +enum ResetPasswordError { + DatabaseError, + TokenGenerationError, + Unauthorized, +} + +impl std::fmt::Display for ResetPasswordError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResetPasswordError::DatabaseError => { + write!(f, "Internal server error") + } + ResetPasswordError::TokenGenerationError => { + write!(f, "Internal server error") + } + ResetPasswordError::Unauthorized => { + write!(f, "Unauthorized, invalid token.") + } + } + } +} + +impl ResponseError for ResetPasswordError { + fn status_code(&self) -> StatusCode { + match self { + ResetPasswordError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::TokenGenerationError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::Unauthorized => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +async fn get_user_id(pool: &sqlx::pool::Pool, token: &str) -> Option { + todo!(); + Some(0) +} + +async fn reset_pwd( + pool: &sqlx::pool::Pool, + new_password: &str, +) -> Result<(), sqlx::Error> { + todo!(); + Ok(()) +} + +async fn reset_password_trigger( + state: web::Data, + view: ResetPasswordView, + ip_adress: std::net::IpAddr, +) -> Result<(String, String), ResetPasswordError> { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(ResetPasswordError::DatabaseError), + }; + + let user_id = match get_user_id(&pool, view.token()).await { + Some(user_id) => user_id, + None => return Err(ResetPasswordError::Unauthorized), + }; + + reset_pwd(&pool, view.new_password()) + .await + .map_err(|e| ResetPasswordError::DatabaseError)?; + + match generate_session(user_id, &view.device_info(), ip_adress, state).await { + Ok((jwt, refresh_token)) => Ok((jwt, refresh_token)), + _ => Err(ResetPasswordError::TokenGenerationError), + } +} + +#[utoipa::path( + post, + path = "/", + responses( + (status = 200, description = "Password reset successfully", body = ResetPasswordResponseView), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized, invalid token"), + (status = 500, description = "Internal server error") + ), + tag = "Auth", +)] +#[post("/reset_password")] +pub async fn reset_password( + state: web::Data, + body: web::Json, + conn: ConnectionInfo, +) -> Result { + let ip_str = conn.realip_remote_addr().unwrap_or("unknown").to_string(); + let ip_address = std::net::IpAddr::from( + ip_str + .parse::() + .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))), + ); + let (jwt, refresh_token) = reset_password_trigger(state, body.into_inner(), ip_address).await?; + + Ok(HttpResponse::Ok() + .append_header(("Authorization", format!("Bearer {}", jwt))) + .json(ResetPasswordResponseView::from(refresh_token))) +} diff --git a/src/endpoints/v1/auth/reset_password/mod.rs b/src/endpoints/v1/auth/reset_password/mod.rs new file mode 100644 index 0000000..cf494a1 --- /dev/null +++ b/src/endpoints/v1/auth/reset_password/mod.rs @@ -0,0 +1,3 @@ +pub mod endpoint; +pub mod view; +pub mod doc; diff --git a/src/endpoints/v1/auth/reset_password/view.rs b/src/endpoints/v1/auth/reset_password/view.rs new file mode 100644 index 0000000..13075a6 --- /dev/null +++ b/src/endpoints/v1/auth/reset_password/view.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use utoipa::ToSchema; + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ResetPasswordView { + token: String, + new_password: String, + device_info: String, +} + +impl ResetPasswordView { + pub fn token(&self) -> &str { + &self.token + } + + pub fn new_password(&self) -> &str { + &self.new_password + } + + pub fn device_info(&self) -> String { + self.device_info.clone() + } +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ResetPasswordResponseView { + refresh_token: String, +} + +impl ResetPasswordResponseView { + pub fn new(refresh_token: String) -> Self { + ResetPasswordResponseView { refresh_token } + } + + pub fn refresh_token(&self) -> &str { + &self.refresh_token + } +} + +impl Display for ResetPasswordResponseView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ResetPasswordResponseView {{ refresh_token: {} }}", + self.refresh_token + ) + } +} + +impl From for ResetPasswordResponseView { + fn from(token: String) -> Self { + ResetPasswordResponseView { + refresh_token: token, + } + } +} diff --git a/tests/queries/auth/login.rs b/tests/queries/auth/login.rs index 8736a09..e44fca3 100644 --- a/tests/queries/auth/login.rs +++ b/tests/queries/auth/login.rs @@ -17,7 +17,7 @@ async fn test_login_user_success() { assert_eq!( result.unwrap(), - LoginUserQueryResultView::new(1, "password123".to_string()) + LoginUserQueryResultView::new(1, "password123".to_string(), true) ); } From 569a71c2a3b0d67f2cd26703b4021ac69e5b8945 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 19 May 2026 18:18:53 +0800 Subject: [PATCH 2/4] feat: add password reset --- Cargo.lock | 669 +++++++++++------- Cargo.toml | 2 +- docker-compose.yml | 21 +- entrypoint.sh | 2 +- src/database/auth/change_password/mod.rs | 4 +- src/database/auth/change_password/query.rs | 19 +- src/database/auth/change_password/view.rs | 33 +- src/database/get_user_id/mod.rs | 5 + src/database/get_user_id/query.rs | 16 + src/database/get_user_id/view.rs | 30 + src/database/mod.rs | 1 + .../v1/auth/forgot_password/endpoint.rs | 147 +++- src/endpoints/v1/auth/login/endpoint.rs | 2 +- .../v1/auth/reset_password/endpoint.rs | 85 ++- src/lib.rs | 66 ++ 15 files changed, 797 insertions(+), 305 deletions(-) create mode 100644 src/database/get_user_id/mod.rs create mode 100644 src/database/get_user_id/query.rs create mode 100644 src/database/get_user_id/view.rs diff --git a/Cargo.lock b/Cargo.lock index 6d42117..a0944fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,15 +21,15 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "base64 0.22.1", + "base64", "bitflags", "brotli", "bytes", @@ -49,8 +49,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.3", - "sha1", + "rand 0.10.1", + "sha1 0.11.0", "smallvec", "tokio", "tokio-util", @@ -253,9 +253,9 @@ checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" [[package]] name = "astral-tokio-tar" -version = "0.5.6" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5" +checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" dependencies = [ "filetime", "futures-core", @@ -334,9 +334,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -390,12 +390,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -410,9 +404,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] @@ -426,6 +420,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bollard" version = "0.20.2" @@ -433,7 +436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", - "base64 0.22.1", + "base64", "bitflags", "bollard-buildkit-proto", "bollard-stubs", @@ -452,7 +455,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.3", + "rand 0.9.4", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -490,7 +493,7 @@ version = "1.52.1-rc.29.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" dependencies = [ - "base64 0.22.1", + "base64", "bollard-buildkit-proto", "bytes", "prost", @@ -521,6 +524,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -541,18 +553,18 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] [[package]] name = "cc" -version = "1.2.57" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -574,7 +586,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -591,6 +603,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "combine" version = "4.6.7" @@ -620,6 +638,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.10.0" @@ -662,9 +686,10 @@ version = "0.1.0" dependencies = [ "actix-web", "async-trait", - "base64 0.22.1", + "base64", "chrono", "futures-util", + "lettre", "mairie360_api_lib", "once_cell", "rand 0.10.1", @@ -708,9 +733,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -758,6 +783,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -767,7 +810,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -855,7 +898,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -910,12 +953,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -929,11 +984,11 @@ dependencies = [ [[package]] name = "docker_credential" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +checksum = "a4564c274ebf369f501de192b02a0b81a5c4bda375abfe526aa70fc702fa6fa0" dependencies = [ - "base64 0.21.7", + "base64", "serde", "serde_json", ] @@ -957,7 +1012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -983,7 +1038,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -1005,7 +1060,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -1018,6 +1073,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1091,14 +1162,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "ferroid" -version = "0.8.9" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.9.3", + "rand 0.10.1", "web-time", ] @@ -1120,13 +1197,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1320,7 +1396,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -1348,7 +1424,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1357,9 +1433,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1367,7 +1443,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1393,9 +1469,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" @@ -1430,7 +1506,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1439,7 +1515,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -1507,24 +1592,32 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1547,15 +1640,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1635,12 +1727,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1648,9 +1741,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1661,9 +1754,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1675,15 +1768,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1695,15 +1788,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1739,9 +1832,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1766,12 +1859,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1812,33 +1905,36 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ - "base64 0.22.1", + "base64", "ed25519-dalek", "getrandom 0.2.17", - "hmac", + "hmac 0.12.1", "js-sys", "p256", "p384", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "signature", + "zeroize", ] [[package]] @@ -1862,11 +1958,38 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lettre" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" +dependencies = [ + "async-trait", + "base64", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "httpdate", + "idna", + "mime", + "nom", + "percent-encoding", + "quoted_printable", + "rustls", + "socket2 0.6.3", + "tokio", + "tokio-rustls", + "url", + "webpki-roots 1.0.7", +] + [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -1876,14 +1999,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] [[package]] @@ -1904,9 +2027,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-channel" @@ -1977,7 +2100,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", ] [[package]] @@ -2024,6 +2157,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num" version = "0.4.3" @@ -2059,7 +2201,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -2075,9 +2217,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -2169,7 +2311,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -2181,7 +2323,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -2274,18 +2416,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -2298,12 +2440,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkcs1" version = "0.7.5" @@ -2327,9 +2463,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -2345,19 +2481,19 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-protocol" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" dependencies = [ - "base64 0.22.1", + "base64", "byteorder", "bytes", "fallible-iterator", - "hmac", - "md-5", + "hmac 0.13.0", + "md-5 0.11.0", "memchr", - "rand 0.9.3", - "sha2", + "rand 0.10.1", + "sha2 0.11.0", "stringprep", ] @@ -2375,9 +2511,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2466,6 +2602,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" + [[package]] name = "r-efi" version = "5.3.0" @@ -2480,9 +2622,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2491,9 +2633,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -2507,7 +2649,7 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -2550,15 +2692,15 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redis" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e41a79ae5cbb41257d84cf4cf0db0bb5a95b11bf05c62c351de4fe748620d" +checksum = "72d32a1ac9123f0d84fda64bfc02a271d9868483162dd2d9099b5c362ece064c" dependencies = [ "arcstr", "async-lock", @@ -2590,9 +2732,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ "bitflags", ] @@ -2658,7 +2800,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -2682,8 +2824,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2726,15 +2868,15 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2760,9 +2902,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -2787,18 +2929,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2919,9 +3061,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2972,7 +3114,7 @@ version = "0.9.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -3015,15 +3157,16 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ - "base64 0.22.1", + "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -3034,9 +3177,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling", "proc-macro2", @@ -3078,7 +3221,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3095,7 +3249,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3120,21 +3285,21 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3209,7 +3374,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "chrono", "crc", @@ -3222,7 +3387,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.13.0", + "indexmap 2.14.0", "ipnetwork", "log", "memchr", @@ -3231,7 +3396,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror", "tokio", @@ -3270,7 +3435,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -3287,13 +3452,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -3303,18 +3468,18 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", - "sha1", - "sha2", + "sha1 0.10.6", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -3331,7 +3496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.22.1", + "base64", "bitflags", "byteorder", "chrono", @@ -3343,18 +3508,18 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "ipnetwork", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -3472,9 +3637,9 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.27.1" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c0624faaa317c56d6d19136580be889677259caf5c897941c6f446b4655068" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" dependencies = [ "astral-tokio-tar", "async-trait", @@ -3554,9 +3719,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3628,7 +3793,7 @@ dependencies = [ "socket2 0.6.3", "tokio", "tokio-util", - "whoami 2.1.1", + "whoami 2.1.2", ] [[package]] @@ -3667,15 +3832,15 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", - "base64 0.22.1", + "base64", "bytes", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body", "http-body-util", @@ -3696,9 +3861,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -3713,7 +3878,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -3776,9 +3941,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" @@ -3815,9 +3980,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -3843,7 +4008,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "base64 0.22.1", + "base64", "log", "percent-encoding", "rustls", @@ -3858,7 +4023,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ - "base64 0.22.1", + "base64", "http 1.4.0", "httparse", "log", @@ -3895,7 +4060,7 @@ version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_json", "serde_norway", @@ -3921,7 +4086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ "actix-web", - "base64 0.22.1", + "base64", "mime_guess", "regex", "rust-embed", @@ -3992,11 +4157,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -4005,7 +4170,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -4025,9 +4190,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -4038,9 +4203,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4048,9 +4213,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -4061,9 +4226,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -4085,7 +4250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -4098,15 +4263,15 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -4128,14 +4293,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -4152,9 +4317,9 @@ dependencies = [ [[package]] name = "whoami" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" dependencies = [ "libc", "libredox", @@ -4410,6 +4575,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -4429,7 +4600,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -4460,7 +4631,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -4479,7 +4650,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -4491,9 +4662,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xattr" @@ -4513,9 +4684,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4524,9 +4695,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4536,18 +4707,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -4556,18 +4727,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4580,12 +4751,26 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4594,9 +4779,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4605,9 +4790,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -4623,7 +4808,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.13.0", + "indexmap 2.14.0", "memchr", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index 96bcef5..e73562b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ async-trait = "0.1.89" base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" +lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "tokio1-rustls", "ring", "webpki-roots", "builder"] } mairie360_api_lib = "0.6.0" rand = "0.10" serde = { version = "1.0.219", features = ["derive"] } @@ -24,7 +25,6 @@ uuid = { version = "1.0", features = ["v4", "serde"] } serial_test = "3.3.1" once_cell = "1.21.3" - [profile.dev] opt-level = 0 diff --git a/docker-compose.yml b/docker-compose.yml index 6882358..4a9ce80 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,6 +100,7 @@ services: build: context: . dockerfile: development.Dockerfile + restart: always develop: watch: - action: sync @@ -125,6 +126,11 @@ services: PUBLIC_URL: http://core.development.mairie360.fr JWT_SECRET: b"secret" JWT_TIMEOUT: 3600 + SMTP_HOST: "mailpit" # Nom du service docker + SMTP_PORT: "1025" # Port SMTP de mailpit + SMTP_USERNAME: "" # Pas besoin d'authentification en local + SMTP_PASSWORD: "" # Pas besoin d'authentification en local + EMAIL_FROM: "noreply.mairie360@dev.local" depends_on: liquibase: condition: service_completed_successfully @@ -137,6 +143,19 @@ services: retries: 5 start_period: 90s + mailpit: + image: axllent/mailpit:v1.15 + container_name: mairie360-mailpit + restart: always + ports: + - "8025:8025" # Interface Web pour voir les mails (http://localhost:8025) + - "1025:1025" # Le port SMTP pour ton code Rust + networks: + - backend + depends_on: + core: + condition: service_healthy + nginx: image: nginx:1.29.1-bookworm restart: always @@ -147,5 +166,5 @@ services: networks: - backend depends_on: - core: + mailpit: condition: service_healthy diff --git a/entrypoint.sh b/entrypoint.sh index e426408..5377cbd 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,4 +5,4 @@ set -e cd /usr/src/core # Lancer cargo watch -exec cargo watch -w src -i target -x run \ No newline at end of file +exec cargo watch --poll -w src -i target -x run diff --git a/src/database/auth/change_password/mod.rs b/src/database/auth/change_password/mod.rs index 8d95cfe..7124855 100644 --- a/src/database/auth/change_password/mod.rs +++ b/src/database/auth/change_password/mod.rs @@ -1,5 +1,5 @@ mod query; -pub use query::login_query; +pub use query::change_password_query; mod view; -pub use view::{LoginUserQueryResultView, LoginUserQueryView}; +pub use view::ChangePasswordQueryView; diff --git a/src/database/auth/change_password/query.rs b/src/database/auth/change_password/query.rs index 2ed6718..b61845c 100644 --- a/src/database/auth/change_password/query.rs +++ b/src/database/auth/change_password/query.rs @@ -1,17 +1,18 @@ -use crate::database::auth::login::LoginUserQueryResultView; -use crate::database::auth::login::LoginUserQueryView; use mairie360_api_lib::database::db_interface::DatabaseQueryView; use mairie360_api_lib::database::errors::DatabaseError; use sqlx::PgPool; -pub async fn login_query( - view: LoginUserQueryView, +use crate::database::auth::change_password::ChangePasswordQueryView; + +pub async fn change_password_query( + view: ChangePasswordQueryView, pool: PgPool, -) -> Result, DatabaseError> { - let result = sqlx::query_as::<_, LoginUserQueryResultView>(&view.get_request()) - .bind(view.get_email()) - .fetch_optional(&pool) +) -> Result<(), DatabaseError> { + sqlx::query(&view.get_request()) + .bind(view.get_password()) + .bind(view.get_user_id() as i32) + .execute(&pool) .await?; - Ok(result) + Ok(()) } diff --git a/src/database/auth/change_password/view.rs b/src/database/auth/change_password/view.rs index 420d937..cb5d3e9 100644 --- a/src/database/auth/change_password/view.rs +++ b/src/database/auth/change_password/view.rs @@ -1,38 +1,37 @@ use mairie360_api_lib::database::db_interface::DatabaseQueryView; use std::fmt::Display; -pub struct LoginUserQueryView { - email: String, +pub struct ChangePasswordQueryView { password: String, + user_id: u64, } -impl LoginUserQueryView { - pub fn new(email: String, password: String) -> Self { - Self { email, password } +impl ChangePasswordQueryView { + pub fn new(password: &str, user_id: u64) -> Self { + Self { + password: password.to_string(), + user_id, + } } - pub fn get_email(&self) -> &String { - &self.email + pub fn get_password(&self) -> &str { + &self.password } - pub fn get_password(&self) -> &String { - &self.password + pub fn get_user_id(&self) -> u64 { + self.user_id } } -impl DatabaseQueryView for LoginUserQueryView { +impl DatabaseQueryView for ChangePasswordQueryView { fn get_request(&self) -> String { - "SELECT id, password, first_connect FROM users WHERE email = $1".to_string() + "UPDATE users SET password = $1 WHERE id = $2".to_string() } } -impl Display for LoginUserQueryView { +impl Display for ChangePasswordQueryView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "LoginUserQueryView: email = {}, password = [PROTECTED]", - self.email - ) + write!(f, "ChangePasswordQueryView: password = [PROTECTED]") } } diff --git a/src/database/get_user_id/mod.rs b/src/database/get_user_id/mod.rs new file mode 100644 index 0000000..5d2ef55 --- /dev/null +++ b/src/database/get_user_id/mod.rs @@ -0,0 +1,5 @@ +mod query; +pub use query::get_user_id_query; + +mod view; +pub use view::GetUserIdQueryView; diff --git a/src/database/get_user_id/query.rs b/src/database/get_user_id/query.rs new file mode 100644 index 0000000..15051d6 --- /dev/null +++ b/src/database/get_user_id/query.rs @@ -0,0 +1,16 @@ +use crate::database::get_user_id::view::GetUserIdQueryView; +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use mairie360_api_lib::database::errors::DatabaseError; +use sqlx::PgPool; + +pub async fn get_user_id_query( + view: GetUserIdQueryView, + pool: PgPool, +) -> Result { + let result = sqlx::query_scalar(&view.get_request()) + .bind(view.email()) + .fetch_one(&pool) + .await?; + + Ok(result) +} diff --git a/src/database/get_user_id/view.rs b/src/database/get_user_id/view.rs new file mode 100644 index 0000000..d1b3bdc --- /dev/null +++ b/src/database/get_user_id/view.rs @@ -0,0 +1,30 @@ +use mairie360_api_lib::database::db_interface::DatabaseQueryView; +use std::fmt::Display; + +pub struct GetUserIdQueryView { + email: String, +} + +impl GetUserIdQueryView { + pub fn new(email: &str) -> Self { + Self { + email: email.to_string(), + } + } + + pub fn email(&self) -> &str { + &self.email + } +} + +impl DatabaseQueryView for GetUserIdQueryView { + fn get_request(&self) -> String { + "SELECT id FROM users WHERE email = $1".to_string() + } +} + +impl Display for GetUserIdQueryView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GetUserIdQueryView: email = {}", self.email) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 5d4dfe7..22b1d54 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod get_user_id; pub mod groups; pub mod ressources; pub mod rights; diff --git a/src/endpoints/v1/auth/forgot_password/endpoint.rs b/src/endpoints/v1/auth/forgot_password/endpoint.rs index c9dc973..bfd8367 100644 --- a/src/endpoints/v1/auth/forgot_password/endpoint.rs +++ b/src/endpoints/v1/auth/forgot_password/endpoint.rs @@ -1,21 +1,44 @@ -use crate::database::roles::get_roles::{get_roles_query, GetRolesQueryView}; +use crate::database::auth::is_first_time::{is_first_time_query, IsFirstTimeQueryView}; +use crate::database::get_user_id::{get_user_id_query, GetUserIdQueryView}; use crate::endpoints::v1::auth::forgot_password::view::ForgotPasswordView; +use crate::{build_email, get_email_sender, send_email, EmailDestination}; use actix_web::http::StatusCode; -use actix_web::{get, web, HttpResponse, Responder, ResponseError}; +use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::database::queries::does_user_exist_by_email_query; +use mairie360_api_lib::database::query_views::DoesUserExistByEmailQueryView; +use mairie360_api_lib::pool::redis::simple_key::secured::{handle_secure_get, handle_secure_post}; use mairie360_api_lib::pool::AppState; +use sqlx::PgPool; +use uuid::Uuid; #[derive(Debug, Clone, PartialEq)] enum ResetPasswordError { + AlreadyRequested, DatabaseError, + MailError, + RedisError, + UserFirstTimeError, UserNotFound, } impl std::fmt::Display for ResetPasswordError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + ResetPasswordError::AlreadyRequested => { + write!(f, "Password reset already requested.") + } ResetPasswordError::DatabaseError => { write!(f, "An error occurred while accessing the database.") } + ResetPasswordError::MailError => { + write!(f, "An error occurred while sending the email.") + } + ResetPasswordError::RedisError => { + write!(f, "An error occurred while accessing Redis.") + } + ResetPasswordError::UserFirstTimeError => { + write!(f, "User not valid.") + } ResetPasswordError::UserNotFound => { write!(f, "User not found.") } @@ -26,7 +49,11 @@ impl std::fmt::Display for ResetPasswordError { impl ResponseError for ResetPasswordError { fn status_code(&self) -> StatusCode { match self { + ResetPasswordError::AlreadyRequested => StatusCode::CONFLICT, ResetPasswordError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::MailError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::RedisError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::UserFirstTimeError => StatusCode::UNAUTHORIZED, ResetPasswordError::UserNotFound => StatusCode::NOT_FOUND, } } @@ -36,34 +63,126 @@ impl ResponseError for ResetPasswordError { } } -async fn check_user(pool: &sqlx::Pool, email: &str) -> bool { - todo!() +async fn check_user(pool: PgPool, email: &str) -> Result<(), ResetPasswordError> { + println!("email: {}", email); + let view = DoesUserExistByEmailQueryView::new(email.to_string()); + let result = does_user_exist_by_email_query(view, pool.clone()).await; + match result { + Ok(true) => {} + _ => return Err(ResetPasswordError::UserNotFound), + }; + + let view = GetUserIdQueryView::new(email); + let user_id = match get_user_id_query(view, pool.clone()).await { + Ok(user_id) => user_id, + Err(_) => return Err(ResetPasswordError::DatabaseError), + }; + + let view = IsFirstTimeQueryView::new(user_id as u64); + let result = is_first_time_query(view, pool).await.unwrap(); + if result { + Ok(()) + } else { + Err(ResetPasswordError::UserFirstTimeError) + } } -async fn trigger(pool: &sqlx::Pool, email: &str) -> Result<(), ResetPasswordError> { - todo!() +async fn handle_forgot_password( + temporary_token: String, + dest: &str, +) -> Result<(), ResetPasswordError> { + // Étape 1 : On récupère où envoyer le mail (les adresses fixes de la CI) + let destination = EmailDestination { + from: match get_email_sender() { + Ok(sender) => sender, + Err(e) => { + eprintln!("Email Sender Error: {}", e); + return Err(ResetPasswordError::MailError); + } + }, + to: dest.to_string(), + }; + + // Préparation du contenu du mail + let subject = "Réinitialisation de votre mot de passe"; + let body = format!( + "Bonjour, voici votre jeton de réinitialisation : {}", + temporary_token + ); + + // Étape 2 : On construit le mail avec les bonnes infos + let email = match build_email(&destination, subject, &body) { + Ok(email) => email, + Err(e) => { + eprintln!("Email Build Error: {}", e); + return Err(ResetPasswordError::MailError); + } + }; + + // Étape 3 : On l'envoie via le serveur SMTP + match send_email(email).await { + Ok(_) => Ok(()), + Err(e) => { + eprintln!("Mail Error: {}", e); + Err(ResetPasswordError::MailError) + } + } +} + +async fn trigger(state: web::Data, email: &str) -> Result<(), ResetPasswordError> { + let token = Uuid::new_v4().to_string(); + handle_secure_post( + state.get_redis_conn().await.unwrap(), + &format!("{}/forgot_password_token", email), + &token, + ) + .await + .map_err(|e| { + eprintln!("Redis Error: {}", e); + ResetPasswordError::RedisError + })?; + handle_secure_post( + state.get_redis_conn().await.unwrap(), + &format!("{}/forgot_password_email", token), + &format!("{}", email), + ) + .await + .map_err(|e| { + eprintln!("Redis Error: {}", e); + ResetPasswordError::RedisError + })?; + match handle_forgot_password(token, email).await { + Ok(_) => Ok(()), + Err(_) => Err(ResetPasswordError::MailError), + } } async fn forgot_password_trigger( state: web::Data, view: ForgotPasswordView, ) -> Result<(), ResetPasswordError> { + let token = handle_secure_get( + state.get_redis_conn().await.unwrap(), + &format!("{}/forgot_password_token", view.email()), + ) + .await; + if token.is_ok() { + return Err(ResetPasswordError::AlreadyRequested); + } + let pool = match state.db_pool.clone() { Some(pool) => pool, None => return Err(ResetPasswordError::DatabaseError), }; - if !check_user(&pool, view.email()).await { - return Err(ResetPasswordError::UserNotFound); + match check_user(pool.clone(), view.email()).await { + Err(err) => Err(err), + _ => trigger(state, view.email()).await, } - - trigger(&pool, view.email()).await?; - - Ok(()) } #[utoipa::path( - get, + post, path = "/", responses( (status = 200, description = "Forgot password request sent successfully"), @@ -76,7 +195,7 @@ async fn forgot_password_trigger( ("jwt" = []) ) )] -#[get("/forgot_password")] +#[post("/forgot_password")] pub async fn forgot_password( state: web::Data, body: web::Json, diff --git a/src/endpoints/v1/auth/login/endpoint.rs b/src/endpoints/v1/auth/login/endpoint.rs index b66d431..4e8626d 100644 --- a/src/endpoints/v1/auth/login/endpoint.rs +++ b/src/endpoints/v1/auth/login/endpoint.rs @@ -10,7 +10,7 @@ use base64::{engine::general_purpose, Engine as _}; use mairie360_api_lib::jwt_manager::generate_jwt; use mairie360_api_lib::pool::redis::simple_key::secured::{handle_secure_get, handle_secure_post}; use mairie360_api_lib::pool::AppState; -use rand::{rng, Rng}; +use rand::fill; use uuid::Uuid; #[derive(Debug, Clone, PartialEq)] diff --git a/src/endpoints/v1/auth/reset_password/endpoint.rs b/src/endpoints/v1/auth/reset_password/endpoint.rs index 34faf20..0efee47 100644 --- a/src/endpoints/v1/auth/reset_password/endpoint.rs +++ b/src/endpoints/v1/auth/reset_password/endpoint.rs @@ -1,3 +1,5 @@ +use crate::database::auth::change_password::{change_password_query, ChangePasswordQueryView}; +use crate::database::get_user_id::{get_user_id_query, GetUserIdQueryView}; use crate::endpoints::v1::auth::login::endpoint::generate_session; use crate::endpoints::v1::auth::reset_password::view::{ ResetPasswordResponseView, ResetPasswordView, @@ -5,13 +7,16 @@ use crate::endpoints::v1::auth::reset_password::view::{ use actix_web::dev::ConnectionInfo; use actix_web::http::StatusCode; use actix_web::{post, web, HttpResponse, Responder, ResponseError}; +use mairie360_api_lib::pool::redis::simple_key::secured::handle_secure_get; +use mairie360_api_lib::pool::redis::simple_key::unsecured::handle_delete_data; use mairie360_api_lib::pool::AppState; #[derive(Debug, Clone, PartialEq)] enum ResetPasswordError { DatabaseError, + RedisError, TokenGenerationError, - Unauthorized, + UnknownToken, } impl std::fmt::Display for ResetPasswordError { @@ -20,11 +25,14 @@ impl std::fmt::Display for ResetPasswordError { ResetPasswordError::DatabaseError => { write!(f, "Internal server error") } + ResetPasswordError::RedisError => { + write!(f, "Internal server error") + } ResetPasswordError::TokenGenerationError => { write!(f, "Internal server error") } - ResetPasswordError::Unauthorized => { - write!(f, "Unauthorized, invalid token.") + ResetPasswordError::UnknownToken => { + write!(f, "Unknown token") } } } @@ -34,8 +42,9 @@ impl ResponseError for ResetPasswordError { fn status_code(&self) -> StatusCode { match self { ResetPasswordError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + ResetPasswordError::RedisError => StatusCode::INTERNAL_SERVER_ERROR, ResetPasswordError::TokenGenerationError => StatusCode::INTERNAL_SERVER_ERROR, - ResetPasswordError::Unauthorized => StatusCode::UNAUTHORIZED, + ResetPasswordError::UnknownToken => StatusCode::UNAUTHORIZED, } } @@ -44,16 +53,27 @@ impl ResponseError for ResetPasswordError { } } -async fn get_user_id(pool: &sqlx::pool::Pool, token: &str) -> Option { - todo!(); - Some(0) +async fn get_user_id(state: web::Data, email: &str) -> Result { + let pool = match state.db_pool.clone() { + Some(pool) => pool, + None => return Err(ResetPasswordError::DatabaseError), + }; + let view = GetUserIdQueryView::new(&email); + match get_user_id_query(view, pool.clone()).await { + Ok(user_id) => Ok(user_id as u64), + Err(_) => Err(ResetPasswordError::DatabaseError), + } } async fn reset_pwd( pool: &sqlx::pool::Pool, new_password: &str, -) -> Result<(), sqlx::Error> { - todo!(); + user_id: u64, +) -> Result<(), ResetPasswordError> { + let view = ChangePasswordQueryView::new(new_password, user_id); + change_password_query(view, pool.clone()) + .await + .map_err(|_| ResetPasswordError::DatabaseError)?; Ok(()) } @@ -64,21 +84,52 @@ async fn reset_password_trigger( ) -> Result<(String, String), ResetPasswordError> { let pool = match state.db_pool.clone() { Some(pool) => pool, - None => return Err(ResetPasswordError::DatabaseError), + None => { + eprintln!("Database pool is not available"); + return Err(ResetPasswordError::DatabaseError); + } }; - let user_id = match get_user_id(&pool, view.token()).await { - Some(user_id) => user_id, - None => return Err(ResetPasswordError::Unauthorized), + let key = format!("{}/forgot_password_email", view.token()); + let email = match handle_secure_get(state.get_redis_conn().await.unwrap(), &key).await { + Ok(email) => email, + Err(e) => { + eprintln!("Failed to get email from Redis: {:?}", e); + return Err(ResetPasswordError::UnknownToken); + } + }; + let user_id = match get_user_id(state.clone(), &email).await { + Ok(user_id) => user_id, + Err(e) => { + eprintln!("Failed to get user ID: {:?}", e); + return Err(e); + } }; - reset_pwd(&pool, view.new_password()) - .await - .map_err(|e| ResetPasswordError::DatabaseError)?; + let reversed_key = format!("{}/forgot_password_token", email); + match handle_delete_data(state.get_redis_conn().await.unwrap(), &reversed_key).await { + Ok(_) => {} + Err(e) => { + eprintln!("Failed to delete reversed key: {:?}", e); + return Err(ResetPasswordError::RedisError); + } + } + match handle_delete_data(state.get_redis_conn().await.unwrap(), &key).await { + Ok(_) => {} + Err(e) => { + eprintln!("Failed to delete key: {:?}", e); + return Err(ResetPasswordError::RedisError); + } + } + + reset_pwd(&pool, view.new_password(), user_id).await?; match generate_session(user_id, &view.device_info(), ip_adress, state).await { Ok((jwt, refresh_token)) => Ok((jwt, refresh_token)), - _ => Err(ResetPasswordError::TokenGenerationError), + Err(e) => { + eprintln!("Failed to generate session: {:?}", e); + Err(ResetPasswordError::TokenGenerationError) + } } } diff --git a/src/lib.rs b/src/lib.rs index 9ba59cb..d560eef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,68 @@ pub mod database; pub mod endpoints; + +use lettre::{ + transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, + Tokio1Executor, +}; +use std::env; + +// Structure pour stocker les informations de destination +pub struct EmailDestination { + pub from: String, + pub to: String, +} + +// Type alias pour clarifier le type du mailer de la lib 'lettre' +pub type SmtpMailer = AsyncSmtpTransport; + +pub fn get_email_sender() -> Result> { + // Récupère les variables d'environnement, avec des valeurs de secours (fallback) au cas où + Ok(env::var("EMAIL_FROM").unwrap_or_else(|_| "Beta App ".to_string())) +} + +pub fn build_email( + destination: &EmailDestination, + subject: &str, + body_content: &str, +) -> Result> { + let email = Message::builder() + .from(destination.from.parse()?) + .to(destination.to.parse()?) + .subject(subject) + .body(body_content.to_string())?; + + Ok(email) +} + +pub async fn send_email(email: Message) -> Result<(), Box> { + // 1. Récupération des identifiants SMTP + let smtp_host = env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string()); + let smtp_port = env::var("SMTP_PORT").unwrap_or_else(|_| "1025".to_string()); + let username = env::var("SMTP_USERNAME").unwrap_or_default(); + let password = env::var("SMTP_PASSWORD").unwrap_or_default(); + + let port: u16 = smtp_port.parse().unwrap_or(1025); + + // 2. Configuration dynamique du transporteur + let mailer: SmtpMailer = + if smtp_host == "mailpit" || smtp_host == "localhost" || username.is_empty() { + // En développement local (Mailpit), on se connecte sans chiffrement TLS + // CORRECTION ICI : builder(...) au lieu de builder_some(...) + AsyncSmtpTransport::::builder_dangerous(&smtp_host) + .port(port) + .build() + } else { + // En production (Resend, SendGrid...), on utilise STARTTLS avec authentification + let creds = Credentials::new(username, password); + AsyncSmtpTransport::::starttls_relay(&smtp_host)? + .credentials(creds) + .port(port) + .build() + }; + + // 3. Envoi effectif + mailer.send(email).await?; + + Ok(()) +} From f9e3c8ed8d0b5288cf82c40cf362f55007756880 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 19 May 2026 18:21:12 +0800 Subject: [PATCH 3/4] fix: fix lint error --- src/endpoints/v1/auth/reset_password/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/v1/auth/reset_password/mod.rs b/src/endpoints/v1/auth/reset_password/mod.rs index cf494a1..b74ee2f 100644 --- a/src/endpoints/v1/auth/reset_password/mod.rs +++ b/src/endpoints/v1/auth/reset_password/mod.rs @@ -1,3 +1,3 @@ +pub mod doc; pub mod endpoint; pub mod view; -pub mod doc; From 4e212bda3f25458f80027a891c51ab4b06d401c9 Mon Sep 17 00:00:00 2001 From: Quentin Tennerel Date: Tue, 19 May 2026 18:49:40 +0800 Subject: [PATCH 4/4] feat: update api_lib version --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0944fc..299b372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,9 +2065,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mairie360_api_lib" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbcc94519080ece64c2abe6e37c7b994327a606401b955a9abb5f8fb910a2283" +checksum = "aee2a3ff978ae410d2f5c54778324ada61060bbc49e5eb9d01bb1e30f6298b8a" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index e73562b..528a184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "tokio1-rustls", "ring", "webpki-roots", "builder"] } -mairie360_api_lib = "0.6.0" +mairie360_api_lib = "0.7.0" rand = "0.10" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140"