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