1use anyhow::Result;
7use chrono::{DateTime, Utc};
8use r2d2::Pool;
9use r2d2_sqlite::SqliteConnectionManager;
10use rusqlite::{params, OptionalExtension};
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13use tracing::info;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct User {
17 pub id: i64,
18 pub email: String,
19 pub name: String,
20 pub password_hash: String,
21 pub email_verified: bool,
22 pub verification_token: Option<String>,
23 pub created_at: DateTime<Utc>,
24 pub verified_at: Option<DateTime<Utc>>,
25 pub google_id: Option<String>,
26 pub avatar_url: Option<String>,
27}
28
29pub struct UsersStore {
30 pool: Arc<Pool<SqliteConnectionManager>>,
31}
32
33impl Clone for UsersStore {
34 fn clone(&self) -> Self {
35 Self {
36 pool: Arc::clone(&self.pool),
37 }
38 }
39}
40
41impl UsersStore {
42 pub fn new(pool: Arc<Pool<SqliteConnectionManager>>) -> Self {
43 Self { pool }
44 }
45
46 pub fn initialize_schema(&self) -> Result<()> {
47 let conn = self.pool.get()?;
48 conn.execute(
49 "CREATE TABLE IF NOT EXISTS users (
50 id INTEGER PRIMARY KEY AUTOINCREMENT,
51 email TEXT NOT NULL UNIQUE,
52 name TEXT NOT NULL,
53 password_hash TEXT NOT NULL,
54 email_verified INTEGER DEFAULT 0,
55 verification_token TEXT,
56 created_at TEXT NOT NULL,
57 verified_at TEXT
58 )",
59 [],
60 )?;
61
62 let _ = conn.execute("ALTER TABLE users ADD COLUMN google_id TEXT", []);
64 let _ = conn.execute("ALTER TABLE users ADD COLUMN avatar_url TEXT", []);
65
66 info!("Users table initialized");
67 Ok(())
68 }
69
70 pub fn create_user(
71 &self,
72 email: &str,
73 name: &str,
74 password_hash: &str,
75 ) -> Result<i64> {
76 let conn = self.pool.get()?;
77 let now = Utc::now().to_rfc3339();
78
79 conn.execute(
80 "INSERT INTO users (email, name, password_hash, email_verified, verification_token, created_at)
81 VALUES (?1, ?2, ?3, 1, '', ?4)",
82 params![email, name, password_hash, now],
83 )?;
84
85 let id = conn.last_insert_rowid();
86 info!("User created with id: {}", id);
87 Ok(id)
88 }
89
90 pub fn upsert_google_user(
99 &self,
100 email: &str,
101 name: &str,
102 google_id: &str,
103 avatar_url: Option<&str>,
104 ) -> Result<(User, bool)> {
105 let conn = self.pool.get()?;
106
107 let mut is_new_user = false;
108
109 let by_google_id = conn
111 .query_row(
112 "SELECT id FROM users WHERE google_id = ?1",
113 params![google_id],
114 |row| row.get::<_, i64>(0),
115 )
116 .optional()?;
117
118 if let Some(user_id) = by_google_id {
119 conn.execute(
121 "UPDATE users SET name = ?1, avatar_url = ?2 WHERE id = ?3",
122 params![name, avatar_url, user_id],
123 )?;
124 } else {
125 let by_email = conn
127 .query_row(
128 "SELECT id FROM users WHERE email = ?1",
129 params![email],
130 |row| row.get::<_, i64>(0),
131 )
132 .optional()?;
133
134 if let Some(user_id) = by_email {
135 conn.execute(
137 "UPDATE users SET google_id = ?1, avatar_url = ?2, email_verified = 1 WHERE id = ?3",
138 params![google_id, avatar_url, user_id],
139 )?;
140 } else {
141 let now = Utc::now().to_rfc3339();
143 conn.execute(
144 "INSERT INTO users (email, name, password_hash, email_verified, google_id, avatar_url, created_at)
145 VALUES (?1, ?2, 'google-oauth-user', 1, ?3, ?4, ?5)",
146 params![email, name, google_id, avatar_url, now],
147 )?;
148 is_new_user = true;
149 }
150 }
151
152 let user = conn.query_row(
154 "SELECT id, email, name, password_hash, email_verified, verification_token,
155 created_at, verified_at, google_id, avatar_url
156 FROM users WHERE email = ?1",
157 params![email],
158 |row| {
159 let created_str: String = row.get(6)?;
160 let verified_str: Option<String> = row.get(7)?;
161 Ok(User {
162 id: row.get(0)?,
163 email: row.get(1)?,
164 name: row.get(2)?,
165 password_hash: row.get(3)?,
166 email_verified: row.get::<_, i32>(4)? != 0,
167 verification_token: row.get(5)?,
168 created_at: DateTime::parse_from_rfc3339(&created_str)
169 .map(|dt| dt.with_timezone(&Utc))
170 .unwrap_or_else(|_| Utc::now()),
171 verified_at: verified_str.and_then(|s| {
172 DateTime::parse_from_rfc3339(&s)
173 .ok()
174 .map(|dt| dt.with_timezone(&Utc))
175 }),
176 google_id: row.get(8)?,
177 avatar_url: row.get(9)?,
178 })
179 },
180 )?;
181
182 info!("Google user upserted: {} (new={})", email, is_new_user);
183 Ok((user, is_new_user))
184 }
185
186 pub fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
187 let conn = self.pool.get()?;
188
189 let result = conn
190 .query_row(
191 "SELECT id, email, name, password_hash, email_verified, verification_token,
192 created_at, verified_at, google_id, avatar_url
193 FROM users WHERE email = ?1",
194 params![email],
195 |row| {
196 let created_str: String = row.get(6)?;
197 let verified_str: Option<String> = row.get(7)?;
198
199 Ok(User {
200 id: row.get(0)?,
201 email: row.get(1)?,
202 name: row.get(2)?,
203 password_hash: row.get(3)?,
204 email_verified: row.get::<_, i32>(4)? != 0,
205 verification_token: row.get(5)?,
206 created_at: DateTime::parse_from_rfc3339(&created_str)
207 .map(|dt| dt.with_timezone(&Utc))
208 .unwrap_or_else(|_| Utc::now()),
209 verified_at: verified_str.and_then(|s| {
210 DateTime::parse_from_rfc3339(&s)
211 .ok()
212 .map(|dt| dt.with_timezone(&Utc))
213 }),
214 google_id: row.get(8)?,
215 avatar_url: row.get(9)?,
216 })
217 },
218 )
219 .optional()?;
220
221 Ok(result)
222 }
223
224 pub fn get_user_by_id(&self, id: i64) -> Result<Option<User>> {
225 let conn = self.pool.get()?;
226
227 let result = conn
228 .query_row(
229 "SELECT id, email, name, password_hash, email_verified, verification_token,
230 created_at, verified_at, google_id, avatar_url
231 FROM users WHERE id = ?1",
232 params![id],
233 |row| {
234 let created_str: String = row.get(6)?;
235 let verified_str: Option<String> = row.get(7)?;
236
237 Ok(User {
238 id: row.get(0)?,
239 email: row.get(1)?,
240 name: row.get(2)?,
241 password_hash: row.get(3)?,
242 email_verified: row.get::<_, i32>(4)? != 0,
243 verification_token: row.get(5)?,
244 created_at: DateTime::parse_from_rfc3339(&created_str)
245 .map(|dt| dt.with_timezone(&Utc))
246 .unwrap_or_else(|_| Utc::now()),
247 verified_at: verified_str.and_then(|s| {
248 DateTime::parse_from_rfc3339(&s)
249 .ok()
250 .map(|dt| dt.with_timezone(&Utc))
251 }),
252 google_id: row.get(8)?,
253 avatar_url: row.get(9)?,
254 })
255 },
256 )
257 .optional()?;
258
259 Ok(result)
260 }
261
262 pub fn verify_email(&self, token: &str) -> Result<Option<User>> {
263 let conn = self.pool.get()?;
264 let now = Utc::now().to_rfc3339();
265
266 let rows_affected = conn.execute(
267 "UPDATE users SET email_verified = 1, verified_at = ?1, verification_token = NULL
268 WHERE verification_token = ?2 AND email_verified = 0",
269 params![now, token],
270 )?;
271
272 if rows_affected > 0 {
273 let user = conn
274 .query_row(
275 "SELECT id, email, name, password_hash, email_verified, verification_token,
276 created_at, verified_at, google_id, avatar_url
277 FROM users WHERE verification_token IS NULL AND email_verified = 1
278 ORDER BY id DESC LIMIT 1",
279 [],
280 |row| {
281 let created_str: String = row.get(6)?;
282 let verified_str: Option<String> = row.get(7)?;
283
284 Ok(User {
285 id: row.get(0)?,
286 email: row.get(1)?,
287 name: row.get(2)?,
288 password_hash: row.get(3)?,
289 email_verified: true,
290 verification_token: None,
291 created_at: DateTime::parse_from_rfc3339(&created_str)
292 .map(|dt| dt.with_timezone(&Utc))
293 .unwrap_or_else(|_| Utc::now()),
294 verified_at: verified_str.and_then(|s| {
295 DateTime::parse_from_rfc3339(&s)
296 .ok()
297 .map(|dt| dt.with_timezone(&Utc))
298 }),
299 google_id: row.get(8)?,
300 avatar_url: row.get(9)?,
301 })
302 },
303 )
304 .optional()?;
305
306 Ok(user)
307 } else {
308 Ok(None)
309 }
310 }
311
312 pub fn email_exists(&self, email: &str) -> Result<bool> {
313 let conn = self.pool.get()?;
314 let count: i64 = conn.query_row(
315 "SELECT COUNT(*) FROM users WHERE email = ?1",
316 params![email],
317 |row| row.get(0),
318 )?;
319 Ok(count > 0)
320 }
321}