[Mig] storage to SQLite pool
[Clean] split message handler dispatch
This commit is contained in:
parent
9b20d159b5
commit
3be1d9f308
18 changed files with 1949 additions and 1700 deletions
|
|
@ -1,4 +1,3 @@
|
|||
use json::{self, JsonValue, number::Number};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -33,29 +32,4 @@ impl Contact {
|
|||
pub fn set_last_message_at(&mut self, p0: i64) {
|
||||
self.last_message_at = Option::from(p0);
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
let mut obj = JsonValue::new_object();
|
||||
obj["user_id"] = JsonValue::Number(Number::from(self.user_id));
|
||||
if let Some(name) = &self.user_name {
|
||||
obj["user_name"] = JsonValue::from(name.as_str());
|
||||
}
|
||||
if let Some(ts) = &self.last_message_at {
|
||||
obj["last_message_at"] = JsonValue::Number(Number::from(*ts));
|
||||
}
|
||||
obj
|
||||
}
|
||||
pub fn from_json(o: &JsonValue) -> Contact {
|
||||
let user_id = o["user_id"].as_i64().unwrap_or(0);
|
||||
|
||||
let user_name = o["user_name"].as_str().map(|s| s.to_string());
|
||||
|
||||
let last_message_at = o["last_message_at"].as_i64();
|
||||
|
||||
Contact {
|
||||
user_id,
|
||||
user_name,
|
||||
last_message_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod contact;
|
||||
pub mod user_community_util;
|
||||
pub mod user_manager;
|
||||
pub mod user_profile;
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
use iota_util::file_util::{load_file, save_file};
|
||||
use json::{self, Array, JsonValue};
|
||||
|
||||
pub struct UserCommunityUtil;
|
||||
|
||||
impl UserCommunityUtil {
|
||||
pub fn add_community(storage_owner: i64, address: String, title: String, position: String) {
|
||||
let file_path = format!("users/{}/", storage_owner);
|
||||
let mut communities = Self::load_array(&file_path, "communities.json");
|
||||
|
||||
let mut community = JsonValue::new_object();
|
||||
community["title"] = JsonValue::String(title);
|
||||
community["address"] = JsonValue::String(address);
|
||||
community["position"] = JsonValue::String(position);
|
||||
|
||||
communities.push(community);
|
||||
|
||||
save_file(
|
||||
&file_path,
|
||||
"communities.json",
|
||||
&JsonValue::Array(communities).to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn remove_community(storage_owner: i64, community_address: String) {
|
||||
let file_path = format!("users/{}/", storage_owner);
|
||||
let communities = Self::load_array(&file_path, "communities.json");
|
||||
|
||||
let filtered: Array = communities
|
||||
.iter()
|
||||
.filter(|entry| entry["address"].as_str() != Some(&community_address))
|
||||
.cloned()
|
||||
.collect();
|
||||
save_file(
|
||||
&file_path,
|
||||
"communities.json",
|
||||
&JsonValue::Array(filtered).to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_communities(storage_owner: i64) -> Array {
|
||||
let file_path = format!("users/{}/", storage_owner);
|
||||
Self::load_array(&file_path, "communities.json")
|
||||
}
|
||||
|
||||
fn load_array(dir: &str, name: &str) -> Array {
|
||||
let content = load_file(dir, name);
|
||||
if content.is_empty() {
|
||||
return Array::new();
|
||||
}
|
||||
|
||||
let parsed = json::parse(&content);
|
||||
match parsed {
|
||||
Ok(JsonValue::Array(arr)) => arr,
|
||||
_ => Array::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ use iota_util::file_util::{has_file, load_file, used_dir_space};
|
|||
use json::{JsonValue, object};
|
||||
use rand::Rng;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// --- UserProfile ---
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UserProfile {
|
||||
pub user_id: i64,
|
||||
pub username: String,
|
||||
|
|
@ -43,26 +43,6 @@ impl UserProfile {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
let mut trusted_apps_obj = json::JsonValue::new_object();
|
||||
for (k, v) in &self.trusted_apps {
|
||||
trusted_apps_obj[k] = v.clone().into();
|
||||
}
|
||||
|
||||
let mut obj = object! {
|
||||
"uuid" => self.user_id,
|
||||
"username" => self.username.clone(),
|
||||
"public_key" => self.public_key.clone(),
|
||||
"private_key_hash" => self.private_key_hash.clone(),
|
||||
"created_at" => self.created_at,
|
||||
"reset_token" => self.reset_token.clone(),
|
||||
"trusted_apps" => trusted_apps_obj,
|
||||
};
|
||||
if let Some(d) = &self.display_name {
|
||||
obj["display_name"] = d.clone().into();
|
||||
}
|
||||
obj
|
||||
}
|
||||
pub fn frontend(&self) -> JsonValue {
|
||||
let mut obj = object! {
|
||||
"uuid" => self.user_id,
|
||||
|
|
@ -78,10 +58,11 @@ impl UserProfile {
|
|||
if has_file("", &format!("{}.tu", self.username.clone())) {
|
||||
obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into();
|
||||
}
|
||||
|
||||
obj
|
||||
}
|
||||
pub async fn from_json(j: &JsonValue) -> Option<Self> {
|
||||
|
||||
/// Legacy JSON import - used when migrating from users.json to SQLite.
|
||||
pub fn from_json(j: &JsonValue) -> Option<Self> {
|
||||
let user_id = j["uuid"].as_i64()?;
|
||||
let username = j["username"].as_str()?.to_string();
|
||||
let public_key = j["public_key"].as_str()?.to_string();
|
||||
|
|
@ -99,7 +80,7 @@ impl UserProfile {
|
|||
}
|
||||
}
|
||||
|
||||
let up = UserProfile {
|
||||
Some(UserProfile {
|
||||
user_id,
|
||||
username,
|
||||
display_name,
|
||||
|
|
@ -108,22 +89,15 @@ impl UserProfile {
|
|||
created_at,
|
||||
reset_token,
|
||||
trusted_apps,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Migrate to Omikron / Wss
|
||||
/* if j.has_key("migrate")
|
||||
|| j.has_key("migrating")
|
||||
|| j.has_key("changing")
|
||||
|| j.has_key("move")
|
||||
|| j.has_key("moving")
|
||||
{
|
||||
if auth_connector::migrate_user(&mut up).await {
|
||||
log_message(format!("[INFO] Migration triggered for {}", up.username));
|
||||
user_manager::set_unique(true);
|
||||
}
|
||||
} */
|
||||
pub fn from_yaml(s: &str) -> Result<Self, serde_yaml::Error> {
|
||||
serde_yaml::from_str(s)
|
||||
}
|
||||
|
||||
Some(up)
|
||||
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||
serde_yaml::to_string(self)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue