koprogo_api/application/dto/
two_factor_dto.rs

1use crate::domain::entities::TwoFactorSecret;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5// ========================================
6// 2FA Setup DTOs
7// ========================================
8
9/// DTO for initiating 2FA setup (returns QR code)
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Setup2FAResponseDto {
12    pub secret: String, // Base32-encoded secret (ONLY returned during setup, never again)
13    pub qr_code_data_url: String, // Data URL for QR code image (data:image/png;base64,...)
14    pub backup_codes: Vec<String>, // 10 plaintext backup codes (ONLY shown once, user must save)
15    pub issuer: String, // "KoproGo"
16    pub account_name: String, // User email or username
17}
18
19/// DTO for enabling 2FA (requires TOTP verification)
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Enable2FADto {
22    pub totp_code: String, // 6-digit TOTP code from authenticator app
23}
24
25/// DTO for 2FA enable response
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Enable2FAResponseDto {
28    pub success: bool,
29    pub message: String,
30    pub enabled_at: DateTime<Utc>,
31}
32
33// ========================================
34// 2FA Verification DTOs
35// ========================================
36
37/// DTO for verifying TOTP code during login
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Verify2FADto {
40    pub totp_code: String, // 6-digit TOTP code OR 8-character backup code
41}
42
43/// DTO for 2FA verification response
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Verify2FAResponseDto {
46    pub success: bool,
47    pub message: String,
48    pub backup_code_used: bool, // True if backup code was used instead of TOTP
49    pub backup_codes_remaining: Option<usize>, // Number of backup codes remaining (if backup code used)
50}
51
52// ========================================
53// 2FA Management DTOs
54// ========================================
55
56/// DTO for disabling 2FA (requires current password)
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Disable2FADto {
59    pub current_password: String, // Require password to disable 2FA
60}
61
62/// DTO for regenerating backup codes (requires TOTP verification)
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct RegenerateBackupCodesDto {
65    pub totp_code: String, // Must verify with TOTP before regenerating
66}
67
68/// DTO for regenerate backup codes response
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct RegenerateBackupCodesResponseDto {
71    pub backup_codes: Vec<String>, // 10 new plaintext backup codes
72    pub regenerated_at: DateTime<Utc>,
73}
74
75/// DTO for 2FA status response
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct TwoFactorStatusDto {
78    pub is_enabled: bool,
79    pub verified_at: Option<DateTime<Utc>>,
80    pub last_used_at: Option<DateTime<Utc>>,
81    pub backup_codes_remaining: usize,
82    pub backup_codes_low: bool,     // True if < 3 codes remaining
83    pub needs_reverification: bool, // True if not used in 90 days
84}
85
86impl From<TwoFactorSecret> for TwoFactorStatusDto {
87    fn from(secret: TwoFactorSecret) -> Self {
88        Self {
89            is_enabled: secret.is_enabled,
90            verified_at: secret.verified_at,
91            last_used_at: secret.last_used_at,
92            backup_codes_remaining: secret.backup_codes_encrypted.len(),
93            backup_codes_low: secret.backup_codes_low(),
94            needs_reverification: secret.needs_reverification(),
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use uuid::Uuid;
103
104    #[test]
105    fn test_two_factor_status_dto_from_entity() {
106        let mut secret =
107            TwoFactorSecret::new(Uuid::new_v4(), "JBSWY3DPEHPK3PXP".to_string()).unwrap();
108
109        secret.enable().unwrap();
110
111        let dto: TwoFactorStatusDto = secret.into();
112
113        assert!(dto.is_enabled);
114        assert!(dto.verified_at.is_some());
115        assert_eq!(dto.backup_codes_remaining, 10);
116        assert!(!dto.backup_codes_low);
117        assert!(!dto.needs_reverification);
118    }
119
120    #[test]
121    fn test_two_factor_status_dto_backup_codes_low() {
122        let _codes: Vec<String> = vec!["CODE1".to_string(), "CODE2".to_string()]; // Only 2 codes
123        let mut secret =
124            TwoFactorSecret::new(Uuid::new_v4(), "JBSWY3DPEHPK3PXP".to_string()).unwrap();
125
126        // Remove 8 codes to leave 2
127        for _ in 0..8 {
128            secret.remove_backup_code(0).unwrap();
129        }
130
131        let dto: TwoFactorStatusDto = secret.into();
132
133        assert_eq!(dto.backup_codes_remaining, 2);
134        assert!(dto.backup_codes_low);
135    }
136}