koprogo_api/infrastructure/web/handlers/
two_factor_handlers.rs1use crate::application::dto::{
2 Disable2FADto, Enable2FADto, RegenerateBackupCodesDto, Verify2FADto,
3};
4use crate::infrastructure::web::classification_erreurs;
5use crate::infrastructure::web::middleware::AuthenticatedUser;
6use crate::infrastructure::web::AppState;
7use actix_web::{web, HttpResponse};
8
9pub async fn setup_2fa(auth: AuthenticatedUser, state: web::Data<AppState>) -> HttpResponse {
36 let organization_id = match auth.organization_id {
37 Some(id) => id,
38 None => {
39 return HttpResponse::BadRequest().json(serde_json::json!({
40 "error": "Organization ID is required"
41 }))
42 }
43 };
44
45 match state
46 .two_factor_use_cases
47 .setup_2fa(auth.user_id, organization_id)
48 .await
49 {
50 Ok(response) => HttpResponse::Ok().json(response),
51 Err(e) if e.contains("already enabled") => {
52 HttpResponse::BadRequest().json(serde_json::json!({
53 "error": e
54 }))
55 }
56 Err(e) => {
57 log::error!("Failed to setup 2FA for user: {}", "internal error");
58 let _ = e; HttpResponse::InternalServerError().json(serde_json::json!({
60 "error": "Failed to setup 2FA"
61 }))
62 }
63 }
64}
65
66pub async fn enable_2fa(
89 auth: AuthenticatedUser,
90 dto: web::Json<Enable2FADto>,
91 state: web::Data<AppState>,
92) -> HttpResponse {
93 let organization_id = match auth.organization_id {
94 Some(id) => id,
95 None => {
96 return HttpResponse::BadRequest().json(serde_json::json!({
97 "error": "Organization ID is required"
98 }))
99 }
100 };
101
102 match state
103 .two_factor_use_cases
104 .enable_2fa(auth.user_id, organization_id, dto.into_inner())
105 .await
106 {
107 Ok(response) => HttpResponse::Ok().json(response),
108 Err(e) if e.contains("Invalid TOTP") => {
109 HttpResponse::BadRequest().json(serde_json::json!({
110 "error": "Invalid TOTP code. Please check your authenticator app and try again."
111 }))
112 }
113 Err(e) if e.contains("already enabled") => {
114 HttpResponse::BadRequest().json(serde_json::json!({
115 "error": e
116 }))
117 }
118 Err(e) if classification_erreurs::est_introuvable(&e) => {
119 HttpResponse::BadRequest().json(serde_json::json!({
120 "error": "2FA setup not found. Please run setup first."
121 }))
122 }
123 Err(e) => {
124 log::error!("Failed to enable 2FA for user: {}", "internal error");
125 let _ = e; HttpResponse::InternalServerError().json(serde_json::json!({
127 "error": "Failed to enable 2FA"
128 }))
129 }
130 }
131}
132
133pub async fn verify_2fa(
158 auth: AuthenticatedUser,
159 dto: web::Json<Verify2FADto>,
160 state: web::Data<AppState>,
161) -> HttpResponse {
162 let organization_id = match auth.organization_id {
163 Some(id) => id,
164 None => {
165 return HttpResponse::BadRequest().json(serde_json::json!({
166 "error": "Organization ID is required"
167 }))
168 }
169 };
170
171 match state
172 .two_factor_use_cases
173 .verify_2fa(auth.user_id, organization_id, dto.into_inner())
174 .await
175 {
176 Ok(response) => HttpResponse::Ok().json(response),
177 Err(e) if e.contains("Invalid TOTP") => {
178 HttpResponse::BadRequest().json(serde_json::json!({
179 "error": "Invalid code. Please try again or use a backup code."
180 }))
181 }
182 Err(e) if e.contains("not enabled") => HttpResponse::BadRequest().json(serde_json::json!({
183 "error": "2FA is not enabled for this account"
184 })),
185 Err(e) => {
186 log::error!("Failed to verify 2FA for user: {}", "internal error");
187 let _ = e; HttpResponse::InternalServerError().json(serde_json::json!({
189 "error": "Failed to verify 2FA"
190 }))
191 }
192 }
193}
194
195pub async fn disable_2fa(
218 auth: AuthenticatedUser,
219 dto: web::Json<Disable2FADto>,
220 state: web::Data<AppState>,
221) -> HttpResponse {
222 let organization_id = match auth.organization_id {
223 Some(id) => id,
224 None => {
225 return HttpResponse::BadRequest().json(serde_json::json!({
226 "error": "Organization ID is required"
227 }))
228 }
229 };
230
231 match state
232 .two_factor_use_cases
233 .disable_2fa(auth.user_id, organization_id, dto.into_inner())
234 .await
235 {
236 Ok(_) => HttpResponse::Ok().json(serde_json::json!({
237 "success": true,
238 "message": "2FA successfully disabled"
239 })),
240 Err(e) if e.contains("Invalid password") => {
241 HttpResponse::BadRequest().json(serde_json::json!({
242 "error": "Invalid password. Please verify your password and try again."
243 }))
244 }
245 Err(e) => {
246 log::error!("Failed to disable 2FA for user: {}", "internal error");
247 let _ = e; HttpResponse::InternalServerError().json(serde_json::json!({
249 "error": "Failed to disable 2FA"
250 }))
251 }
252 }
253}
254
255pub async fn regenerate_backup_codes(
287 auth: AuthenticatedUser,
288 dto: web::Json<RegenerateBackupCodesDto>,
289 state: web::Data<AppState>,
290) -> HttpResponse {
291 let organization_id = match auth.organization_id {
292 Some(id) => id,
293 None => {
294 return HttpResponse::BadRequest().json(serde_json::json!({
295 "error": "Organization ID is required"
296 }))
297 }
298 };
299
300 match state
301 .two_factor_use_cases
302 .regenerate_backup_codes(auth.user_id, organization_id, dto.into_inner())
303 .await
304 {
305 Ok(response) => HttpResponse::Ok().json(response),
306 Err(e) if e.contains("Invalid TOTP") => {
307 HttpResponse::BadRequest().json(serde_json::json!({
308 "error": "Invalid TOTP code. Please check your authenticator app and try again."
309 }))
310 }
311 Err(e) if e.contains("not enabled") => HttpResponse::BadRequest().json(serde_json::json!({
312 "error": "2FA is not enabled for this account"
313 })),
314 Err(e) => {
315 log::error!("Failed to regenerate backup codes: internal error");
316 let _ = e; HttpResponse::InternalServerError().json(serde_json::json!({
318 "error": "Failed to regenerate backup codes"
319 }))
320 }
321 }
322}
323
324pub async fn get_2fa_status(auth: AuthenticatedUser, state: web::Data<AppState>) -> HttpResponse {
353 match state
354 .two_factor_use_cases
355 .get_2fa_status(auth.user_id)
356 .await
357 {
358 Ok(status) => HttpResponse::Ok().json(status),
359 Err(e) => {
360 log::error!("Failed to get 2FA status for user: {}", "internal error");
361 let _ = e; HttpResponse::InternalServerError().json(serde_json::json!({
363 "error": "Failed to retrieve 2FA status"
364 }))
365 }
366 }
367}
368
369pub fn configure_two_factor_routes(cfg: &mut web::ServiceConfig) {
371 cfg.service(
372 web::scope("/2fa")
373 .route("/setup", web::post().to(setup_2fa))
374 .route("/enable", web::post().to(enable_2fa))
375 .route("/verify", web::post().to(verify_2fa))
376 .route("/disable", web::post().to(disable_2fa))
377 .route(
378 "/regenerate-backup-codes",
379 web::post().to(regenerate_backup_codes),
380 )
381 .route("/status", web::get().to(get_2fa_status)),
382 );
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::application::ports::{TwoFactorRepository, UserRepository};
389 use crate::application::use_cases::TwoFactorUseCases;
390 use crate::domain::entities::User;
391 use actix_web::{test, web, App};
392 use mockall::mock;
393 use mockall::predicate::*;
394 use std::sync::Arc;
395 use uuid::Uuid;
396
397 mock! {
399 TwoFactorRepo {}
400 #[async_trait::async_trait]
401 impl TwoFactorRepository for TwoFactorRepo {
402 async fn create(&self, secret: &crate::domain::entities::TwoFactorSecret) -> Result<crate::domain::entities::TwoFactorSecret, String>;
403 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<crate::domain::entities::TwoFactorSecret>, String>;
404 async fn update(&self, secret: &crate::domain::entities::TwoFactorSecret) -> Result<crate::domain::entities::TwoFactorSecret, String>;
405 async fn delete(&self, user_id: Uuid) -> Result<(), String>;
406 async fn find_needing_reverification(&self) -> Result<Vec<crate::domain::entities::TwoFactorSecret>, String>;
407 async fn find_with_low_backup_codes(&self) -> Result<Vec<crate::domain::entities::TwoFactorSecret>, String>;
408 }
409 }
410
411 mock! {
412 UserRepo {}
413 #[async_trait::async_trait]
414 impl UserRepository for UserRepo {
415 async fn create(&self, user: &User) -> Result<User, String>;
416 async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, String>;
417 async fn find_by_email(&self, email: &str) -> Result<Option<User>, String>;
418 async fn find_all(&self) -> Result<Vec<User>, String>;
419 async fn find_page(
420 &self,
421 recherche: Option<String>,
422 role: Option<String>,
423 limit: i64,
424 offset: i64,
425 ) -> Result<Vec<User>, String>;
426 async fn count_matching(
427 &self,
428 recherche: Option<String>,
429 role: Option<String>,
430 ) -> Result<i64, String>;
431 async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<User>, String>;
432 async fn update(&self, user: &User) -> Result<User, String>;
433 async fn update_password(&self, id: Uuid, password_hash: &str) -> Result<bool, String>;
434 async fn activate(&self, id: Uuid) -> Result<Option<User>, String>;
435 async fn deactivate(&self, id: Uuid) -> Result<Option<User>, String>;
436 async fn delete(&self, id: Uuid) -> Result<bool, String>;
437 async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String>;
438 }
439 }
440
441 #[actix_web::test]
442 async fn test_get_2fa_status_not_enabled() {
443 let two_factor_repo = Arc::new(MockTwoFactorRepo::new());
444 let user_repo = Arc::new(MockUserRepo::new());
445 let encryption_key: [u8; 32] = [0u8; 32]; let use_cases = Arc::new(TwoFactorUseCases::new(
448 two_factor_repo,
449 user_repo,
450 encryption_key,
451 ));
452
453 let _app = test::init_service(
454 App::new()
455 .app_data(web::Data::new(use_cases))
456 .configure(configure_two_factor_routes),
457 )
458 .await;
459
460 }
463
464 }