1
2
3pub mod email_verification {
5 use rand::Rng;
6 use oxidite_db::sqlx::Row; 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 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 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 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
66pub mod password_reset {
68 use rand::Rng;
69 use oxidite_db::sqlx::Row; 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 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; 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 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 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 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
151pub mod two_factor {
153 use totp_rs::{TOTP, Algorithm};
154 use oxidite_db::sqlx::Row; use rand::Rng;
156
157 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 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 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 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 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 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}