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