Skip to main content

oxidite_auth/
security.rs

1
2
3/// Email verification module
4pub mod email_verification {
5    use rand::Rng;
6    use oxidite_db::sqlx::Row; // ✅ REQUIRED for try_get()
7
8    /// Generate email verification token
9    pub fn generate_token() -> String {
10        let mut rng = rand::rng();
11        let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
12        hex::encode(random_bytes)
13    }
14    
15    /// Store verification token for user
16    pub async fn create_token<D: oxidite_db::Database + ?Sized>(
17        db: &D,
18        user_id: i64,
19    ) -> oxidite_db::Result<String> {
20        let token = generate_token();
21
22        let query = oxidite_db::sqlx::query(
23            "UPDATE users SET verification_token = ? WHERE id = ?"
24        )
25            .bind(&token)
26            .bind(user_id);
27        db.execute_query(query).await?;
28        
29        Ok(token)
30    }
31    
32    /// Verify email with token
33    pub async fn verify_email<D: oxidite_db::Database + ?Sized>(
34        db: &D,
35        token: &str,
36    ) -> oxidite_db::Result<bool> {
37        let query = oxidite_db::sqlx::query(
38            "UPDATE users SET email_verified = 1, verification_token = NULL
39             WHERE verification_token = ?"
40        )
41            .bind(token);
42        let rows = db.execute_query(query).await?;
43        Ok(rows > 0)
44    }
45    
46    /// Check if user email is verified
47    pub async fn is_verified<D: oxidite_db::Database + ?Sized>(
48        db: &D,
49        user_id: i64,
50    ) -> oxidite_db::Result<bool> {
51        let query = oxidite_db::sqlx::query(
52            "SELECT email_verified FROM users WHERE id = ?"
53        )
54            .bind(user_id);
55        let row = db.fetch_one(query).await?;
56        
57        if let Some(row) = row {
58            let verified: i64 = row.try_get("email_verified").unwrap_or(0);
59            Ok(verified == 1)
60        } else {
61            Ok(false)
62        }
63    }
64}
65
66/// Password reset module
67pub mod password_reset {
68    use rand::Rng;
69    use oxidite_db::sqlx::Row; // ✅ REQUIRED
70
71    /// Generate password reset token
72    pub fn generate_token() -> String {
73        let mut rng = rand::rng();
74        let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
75        hex::encode(random_bytes)
76    }
77    
78    /// Create password reset token (valid for 1 hour)
79    pub async fn create_token<D: oxidite_db::Database + ?Sized>(
80        db: &D,
81        user_id: i64,
82    ) -> oxidite_db::Result<String> {
83        let token = generate_token();
84        let now = chrono::Utc::now().timestamp();
85        let expires_at = now + 3600; // 1 hour
86        
87        let query = oxidite_db::sqlx::query(
88            "INSERT INTO password_reset_tokens (user_id, token, expires_at, created_at)
89             VALUES (?, ?, ?, ?)"
90        )
91            .bind(user_id)
92            .bind(&token)
93            .bind(expires_at)
94            .bind(now);
95        db.execute_query(query).await?;
96        
97        Ok(token)
98    }
99    
100    /// Verify reset token and return user_id
101    pub async fn verify_token<D: oxidite_db::Database + ?Sized>(
102        db: &D,
103        token: &str,
104    ) -> oxidite_db::Result<Option<i64>> {
105        let now = chrono::Utc::now().timestamp();
106
107        let query = oxidite_db::sqlx::query(
108            "SELECT user_id FROM password_reset_tokens
109             WHERE token = ? AND expires_at > ?"
110        )
111            .bind(token)
112            .bind(now);
113
114        let row = db.fetch_one(query).await?;
115        
116        if let Some(row) = row {
117            let user_id: i64 = row.try_get("user_id").unwrap_or(0);
118            Ok(Some(user_id))
119        } else {
120            Ok(None)
121        }
122    }
123    
124    /// Consume (delete) reset token
125    pub async fn consume_token<D: oxidite_db::Database + ?Sized>(
126        db: &D,
127        token: &str,
128    ) -> oxidite_db::Result<()> {
129        let query = oxidite_db::sqlx::query(
130            "DELETE FROM password_reset_tokens WHERE token = ?"
131        )
132            .bind(token);
133        db.execute_query(query).await?;
134        Ok(())
135    }
136    
137    /// Clean up expired tokens
138    pub async fn cleanup_expired<D: oxidite_db::Database + ?Sized>(
139        db: &D,
140    ) -> oxidite_db::Result<()> {
141        let now = chrono::Utc::now().timestamp();
142        let query = oxidite_db::sqlx::query(
143            "DELETE FROM password_reset_tokens WHERE expires_at < ?"
144        )
145            .bind(now);
146        db.execute_query(query).await?;
147        Ok(())
148    }
149}
150
151/// Two-Factor Authentication (TOTP) module
152pub mod two_factor {
153    use totp_rs::{TOTP, Algorithm};
154    use oxidite_db::sqlx::Row; // ✅ REQUIRED
155    use rand::Rng;
156
157    /// Generate 2FA secret for user
158    pub fn generate_secret() -> String {
159        use base64::Engine;
160        let mut rng = rand::rng();
161        let random_bytes: Vec<u8> = (0..20).map(|_| rng.random::<u8>()).collect();
162        base64::engine::general_purpose::STANDARD.encode(random_bytes)
163    }
164    
165    /// Enable 2FA for user
166    pub async fn enable<D: oxidite_db::Database + ?Sized>(
167        db: &D,
168        user_id: i64,
169        secret: &str,
170    ) -> oxidite_db::Result<()> {
171        let query = oxidite_db::sqlx::query(
172            "UPDATE users SET two_factor_secret = ?, two_factor_enabled = 1
173             WHERE id = ?"
174        )
175            .bind(secret)
176            .bind(user_id);
177        db.execute_query(query).await?;
178        Ok(())
179    }
180    
181    /// Disable 2FA for user
182    pub async fn disable<D: oxidite_db::Database + ?Sized>(
183        db: &D,
184        user_id: i64,
185    ) -> oxidite_db::Result<()> {
186        let query = oxidite_db::sqlx::query(
187            "UPDATE users SET two_factor_secret = NULL, two_factor_enabled = 0
188             WHERE id = ?"
189        )
190            .bind(user_id);
191        db.execute_query(query).await?;
192        Ok(())
193    }
194    
195    /// Verify TOTP code
196    pub fn verify_code(secret: &str, code: &str) -> bool {
197        use base64::Engine;
198        let secret_bytes = match base64::engine::general_purpose::STANDARD.decode(secret) {
199            Ok(bytes) => bytes,
200            Err(_) => return false,
201        };
202        
203        let totp = match TOTP::new(
204            Algorithm::SHA1,
205            6,
206            1,
207            30,
208            secret_bytes,
209        ) {
210            Ok(t) => t,
211            Err(_) => return false,
212        };
213        
214        totp.check_current(code).unwrap_or(false)
215    }
216    
217    /// Get user's 2FA secret
218    pub async fn get_secret<D: oxidite_db::Database + ?Sized>(
219        db: &D,
220        user_id: i64,
221    ) -> oxidite_db::Result<Option<String>> {
222        let query = oxidite_db::sqlx::query(
223            "SELECT two_factor_secret, two_factor_enabled FROM users WHERE id = ?"
224        )
225            .bind(user_id);
226
227        let row = db.fetch_one(query).await?;
228        
229        if let Some(row) = row {
230            let enabled: i64 = row.try_get("two_factor_enabled").unwrap_or(0);
231            if enabled == 1 {
232                let secret: String = row.try_get("two_factor_secret").unwrap_or_default();
233                if !secret.is_empty() {
234                    return Ok(Some(secret));
235                }
236            }
237        }
238        
239        Ok(None)
240    }
241    
242    /// Generate provisioning URI for TOTP setup (for QR code)
243    pub fn generate_provisioning_uri(secret: &str, account: &str, issuer: &str) -> String {
244        format!(
245            "otpauth://totp/{}:{}?secret={}&issuer={}",
246            urlencoding::encode(issuer),
247            urlencoding::encode(account),
248            secret,
249            urlencoding::encode(issuer)
250        )
251    }
252}