1use std::sync::Arc;
11
12use chrono::{DateTime, Utc};
13
14use super::pin::PinState;
15use super::{Profile, UnlockMethod};
16use crate::storage::db_service::{DatabaseService, Query, QueryParam, RusqliteService};
17
18pub async fn list_profiles(
25 db: &Arc<RusqliteService>,
26 server_id: &str,
27) -> Result<Vec<Profile>, String> {
28 let query = Query::with_params(
29 "SELECT u.id, u.username, u.server_id, u.is_active, u.last_login_at,
30 CASE WHEN p.user_id IS NULL THEN 0 ELSE 1 END AS has_pin
31 FROM users u
32 LEFT JOIN user_pins p ON p.user_id = u.id
33 WHERE u.server_id = ?
34 ORDER BY u.last_login_at DESC",
35 vec![QueryParam::String(server_id.to_string())],
36 );
37
38 db.query_many(query, |row| {
39 let has_pin: i32 = row.get(5)?;
40 Ok(Profile {
41 user_id: row.get(0)?,
42 username: row.get(1)?,
43 server_id: row.get(2)?,
44 avatar_tag: None,
45 unlock_method: if has_pin != 0 {
46 UnlockMethod::Pin
47 } else {
48 UnlockMethod::None
49 },
50 last_used_at: row.get(4)?,
51 is_active: row.get::<_, i32>(3)? != 0,
52 })
53 })
54 .await
55 .map_err(|e| e.to_string())
56}
57
58pub async fn get_profile(
62 db: &Arc<RusqliteService>,
63 user_id: &str,
64) -> Result<Option<Profile>, String> {
65 let query = Query::with_params(
66 "SELECT u.id, u.username, u.server_id, u.is_active, u.last_login_at,
67 CASE WHEN p.user_id IS NULL THEN 0 ELSE 1 END AS has_pin
68 FROM users u
69 LEFT JOIN user_pins p ON p.user_id = u.id
70 WHERE u.id = ?",
71 vec![QueryParam::String(user_id.to_string())],
72 );
73
74 db.query_optional(query, |row| {
75 let has_pin: i32 = row.get(5)?;
76 Ok(Profile {
77 user_id: row.get(0)?,
78 username: row.get(1)?,
79 server_id: row.get(2)?,
80 avatar_tag: None,
81 unlock_method: if has_pin != 0 {
82 UnlockMethod::Pin
83 } else {
84 UnlockMethod::None
85 },
86 last_used_at: row.get(4)?,
87 is_active: row.get::<_, i32>(3)? != 0,
88 })
89 })
90 .await
91 .map_err(|e| e.to_string())
92}
93
94pub async fn get_pin(
98 db: &Arc<RusqliteService>,
99 user_id: &str,
100) -> Result<Option<(String, PinState)>, String> {
101 let query = Query::with_params(
102 "SELECT pin_hash, failed_count, locked_until FROM user_pins WHERE user_id = ?",
103 vec![QueryParam::String(user_id.to_string())],
104 );
105
106 let row: Option<(String, i64, Option<String>)> = db
107 .query_optional(query, |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
108 .await
109 .map_err(|e| e.to_string())?;
110
111 Ok(row.map(|(hash, failed, locked)| {
112 let locked_until = locked
113 .and_then(|s| DateTime::parse_from_rfc3339(&s).ok())
114 .map(|dt| dt.with_timezone(&Utc));
115 (
116 hash,
117 PinState {
118 failed_count: failed.max(0) as u32,
119 locked_until,
120 },
121 )
122 }))
123}
124
125pub async fn set_pin(
129 db: &Arc<RusqliteService>,
130 user_id: &str,
131 pin_hash: &str,
132) -> Result<(), String> {
133 let query = Query::with_params(
134 "INSERT INTO user_pins (user_id, pin_hash, failed_count, locked_until, updated_at)
135 VALUES (?, ?, 0, NULL, CURRENT_TIMESTAMP)
136 ON CONFLICT(user_id) DO UPDATE SET
137 pin_hash = excluded.pin_hash,
138 failed_count = 0,
139 locked_until = NULL,
140 updated_at = CURRENT_TIMESTAMP",
141 vec![
142 QueryParam::String(user_id.to_string()),
143 QueryParam::String(pin_hash.to_string()),
144 ],
145 );
146 db.execute(query).await.map_err(|e| e.to_string())?;
147 Ok(())
148}
149
150pub async fn clear_pin(db: &Arc<RusqliteService>, user_id: &str) -> Result<(), String> {
154 let query = Query::with_params(
155 "DELETE FROM user_pins WHERE user_id = ?",
156 vec![QueryParam::String(user_id.to_string())],
157 );
158 db.execute(query).await.map_err(|e| e.to_string())?;
159 Ok(())
160}
161
162pub async fn save_pin_state(
169 db: &Arc<RusqliteService>,
170 user_id: &str,
171 state: &PinState,
172) -> Result<(), String> {
173 let locked = match state.locked_until {
174 Some(dt) => QueryParam::String(dt.to_rfc3339()),
175 None => QueryParam::Null,
176 };
177 let query = Query::with_params(
178 "UPDATE user_pins SET failed_count = ?, locked_until = ? WHERE user_id = ?",
179 vec![
180 QueryParam::Int64(i64::from(state.failed_count)),
181 locked,
182 QueryParam::String(user_id.to_string()),
183 ],
184 );
185 db.execute(query).await.map_err(|e| e.to_string())?;
186 Ok(())
187}
188
189pub async fn remove_profile(db: &Arc<RusqliteService>, user_id: &str) -> Result<(), String> {
198 let query = Query::with_params(
201 "DELETE FROM users WHERE id = ?",
202 vec![QueryParam::String(user_id.to_string())],
203 );
204 db.execute(query).await.map_err(|e| e.to_string())?;
205 Ok(())
206}