[Mig] storage to SQLite pool

[Clean] split message handler dispatch
This commit is contained in:
Alex Emmet 2026-07-08 23:40:46 +02:00
commit 3be1d9f308
18 changed files with 1949 additions and 1700 deletions

View file

@ -1,15 +1,212 @@
use crate::users::user_profile::UserProfile;
use crate::util::db;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use iota_util::crypto_helper::{self, hex_hash, keyring_from_base64, public_key_bundle_to_base64};
use iota_util::file_util::{load_file, save_file};
use json::JsonValue;
use once_cell::sync::Lazy;
use rusqlite::params;
use rand_core::{OsRng, RngCore};
use std::io::{self};
use std::sync::Mutex;
static USERS: Lazy<Mutex<Vec<UserProfile>>> = Lazy::new(|| Mutex::new(Vec::new()));
static UNIQUE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
pub fn add_user(user: UserProfile) {
if let Err(e) = db::with_db(|conn| {
conn.execute(
r#"
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(user_id) DO UPDATE SET
username = excluded.username,
public_key = excluded.public_key,
private_key_hash = excluded.private_key_hash,
reset_token = excluded.reset_token,
display_name = excluded.display_name
"#,
params![
user.user_id,
user.username,
user.public_key,
user.private_key_hash,
user.reset_token,
user.created_at,
user.display_name,
],
)?;
for (app_id, app_secret) in &user.trusted_apps {
conn.execute(
r#"
INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret)
VALUES (?1, ?2, ?3)
"#,
params![user.user_id, app_id, app_secret],
)?;
}
Ok(())
}) {
eprintln!("Failed to add_user: {}", e);
}
}
pub fn update_user(user: UserProfile) {
add_user(user);
}
pub fn get_user_by_username(username: &str) -> Option<UserProfile> {
match db::with_db(|conn| {
match conn.query_row(
"SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE username = ?1 LIMIT 1",
params![username],
|r| {
let user_id: i64 = r.get(0)?;
Ok(UserProfile {
user_id,
username: r.get(1)?,
display_name: r.get(6)?,
public_key: r.get(2)?,
private_key_hash: r.get(3)?,
created_at: r.get(5)?,
reset_token: r.get(4)?,
trusted_apps: load_trusted_apps(user_id),
})
},
) {
Ok(user) => Ok(Some(user)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}) {
Ok(opt) => opt,
Err(e) => {
eprintln!("Error querying user by username: {}", e);
None
}
}
}
pub fn get_user(user_id: i64) -> Option<UserProfile> {
match db::with_db(|conn| {
match conn.query_row(
"SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE user_id = ?1 LIMIT 1",
params![user_id],
|r| {
let user_id: i64 = r.get(0)?;
Ok(UserProfile {
user_id,
username: r.get(1)?,
display_name: r.get(6)?,
public_key: r.get(2)?,
private_key_hash: r.get(3)?,
created_at: r.get(5)?,
reset_token: r.get(4)?,
trusted_apps: load_trusted_apps(user_id),
})
},
) {
Ok(user) => Ok(Some(user)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}) {
Ok(opt) => opt,
Err(e) => {
eprintln!("Error querying user: {}", e);
None
}
}
}
pub fn get_users() -> Vec<UserProfile> {
match db::with_db(|conn| {
let mut stmt = conn.prepare(
r#"
SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name
FROM users
ORDER BY username
"#,
)?;
let rows = stmt.query_map([], |r| {
let user_id: i64 = r.get(0)?;
let username: String = r.get(1)?;
let public_key: String = r.get(2)?;
let private_key_hash: String = r.get(3)?;
let reset_token: String = r.get(4)?;
let created_at: i64 = r.get(5)?;
let display_name: Option<String> = r.get(6)?;
Ok(UserProfile {
user_id,
username,
display_name,
public_key,
private_key_hash,
created_at,
reset_token,
trusted_apps: std::collections::HashMap::new(),
})
})?;
let mut out = Vec::new();
for row in rows {
match row {
Ok(mut user) => {
user.trusted_apps = load_trusted_apps(user.user_id);
out.push(user);
}
Err(e) => eprintln!("Failed to read user row: {}", e),
}
}
Ok(out)
}) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to query users: {}", e);
Vec::new()
}
}
}
fn load_trusted_apps(user_id: i64) -> std::collections::HashMap<String, String> {
match db::with_db(|conn| {
let mut stmt = conn.prepare(
"SELECT app_id, app_secret FROM trusted_apps WHERE user_id = ?1",
)?;
let rows = stmt.query_map(params![user_id], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
})?;
let mut map = std::collections::HashMap::new();
for row in rows {
if let Ok((k, v)) = row {
map.insert(k, v);
}
}
Ok(map)
}) {
Ok(m) => m,
Err(e) => {
eprintln!("Failed to load trusted apps: {}", e);
std::collections::HashMap::new()
}
}
}
pub fn remove_user(user_id: i64) {
if let Err(e) = db::with_db(|conn| {
conn.execute("DELETE FROM trusted_apps WHERE user_id = ?1", params![user_id])?;
conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?;
Ok(())
}) {
eprintln!("Failed to remove_user: {}", e);
}
}
pub fn clear() {
if let Err(e) = db::with_db(|conn| {
conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users;")?;
Ok(())
}) {
eprintln!("Failed to clear users: {}", e);
}
}
#[allow(dead_code)]
pub async fn load_from_tu(username: &str) -> Result<(), ()> {
@ -35,91 +232,38 @@ pub async fn load_from_tu(username: &str) -> Result<(), ()> {
hex_hash(&keyring_b64),
reset_token,
);
USERS.lock().unwrap().push(user_profile);
add_user(user_profile);
Ok(())
}
pub fn add_user(user: UserProfile) {
USERS.lock().unwrap().push(user);
}
pub fn update_user(user: UserProfile) {
let mut users = USERS.lock().unwrap();
if let Some(pos) = users.iter().position(|u| u.user_id == user.user_id) {
users[pos] = user;
}
*UNIQUE.lock().unwrap() = true;
}
pub fn get_user_by_username(username: &str) -> Option<UserProfile> {
USERS
.lock()
.unwrap()
.iter()
.cloned()
.find(|u| u.username == username)
}
pub fn get_user(user_id: i64) -> Option<UserProfile> {
USERS
.lock()
.unwrap()
.iter()
.cloned()
.find(|u| u.user_id == user_id)
}
pub fn get_users() -> Vec<UserProfile> {
USERS.lock().unwrap().clone()
}
pub fn remove_user(user_id: i64) {
let mut users = USERS.lock().unwrap();
users.retain(|u| u.user_id != user_id);
*UNIQUE.lock().unwrap() = true;
}
pub fn save_users() {
*UNIQUE.lock().unwrap() = false;
let users = USERS.lock().unwrap();
let arr: Vec<JsonValue> = users.iter().map(|u| u.to_json()).collect();
let json_str = JsonValue::Array(arr).dump();
save_file("", "users.json", &json_str);
// No-op: users are auto-saved via SQLite.
}
pub fn clear() {
let mut users = USERS.lock().unwrap();
users.clear();
*UNIQUE.lock().unwrap() = true;
}
pub async fn load_users() -> io::Result<()> {
pub async fn load_users() -> std::io::Result<()> {
// Users are loaded from SQLite on demand. This function is kept for API compat.
// If we need to migrate from a legacy users.json file, we can do so here.
let content = load_file("", "users.json");
if content.trim().is_empty() {
return Ok(());
}
let parsed =
json::parse(&content).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
if let JsonValue::Array(arr) = parsed {
let mut users = USERS.lock().unwrap();
for j in arr.iter() {
if let Some(up) = UserProfile::from_json(j).await {
users.push(up);
if let Ok(parsed) = json::parse(&content) {
if let json::JsonValue::Array(arr) = parsed {
for j in arr.iter() {
if let Some(up) = UserProfile::from_json(j) {
add_user(up);
}
}
}
}
if *UNIQUE.lock().unwrap() {
save_users();
}
// Rename the old file so we don't re-import
let _ = std::fs::rename(
std::path::PathBuf::from(iota_util::file_util::get_directory()).join("users.json"),
std::path::PathBuf::from(iota_util::file_util::get_directory()).join("users.json.imported"),
);
Ok(())
}
#[allow(dead_code)]
pub fn set_unique(val: bool) {
*UNIQUE.lock().unwrap() = val;
}
pub fn save_app_data(user_id: i64, app_identifier: &str, data: &str) {
let path = format!("users/{}/apps", user_id);
let name = format!("{}.json", app_identifier);