diff --git a/core/src/data/local/authorization_request/received.rs b/core/src/data/local/authorization_request/received.rs index 25ef920..18cd36e 100644 --- a/core/src/data/local/authorization_request/received.rs +++ b/core/src/data/local/authorization_request/received.rs @@ -2,7 +2,7 @@ use caretta_id::{DoubleId, SingleId}; use chrono::{DateTime, Local, NaiveDateTime}; use iroh::{NodeId, PublicKey}; -use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; +use crate::{data::local::LocalRecord, global::LOCAL_DATABASE_CONNECTION}; /// Response of node authentication. #[derive(Debug, Clone)] @@ -28,7 +28,7 @@ impl ReceivedAuthorizationRequest { } } -impl LocalModel for ReceivedAuthorizationRequest { +impl LocalRecord for ReceivedAuthorizationRequest { const TABLE_NAME: &str = "received_authorization_request"; const DEFAULT_COLUMNS: &[&str] = &[ "request_id", @@ -37,6 +37,14 @@ impl LocalModel for ReceivedAuthorizationRequest { "created_at", "responded_at", ]; + + type DefaultParams<'a> = (&'a SingleId, &'a [u8;32], &'a str, NaiveDateTime, Option) + where + Self: 'a; + + fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a> { + (&self.request_id,&self.public_key.as_bytes(), &self.node_info, self.created_at.naive_utc(), self.responded_at.map(|x| x.naive_utc())) + } fn from_default_row(row: &rusqlite::Row<'_>) -> Result { let created_at: NaiveDateTime = row.get(3)?; let responded_at: Option = row.get(4)?; @@ -77,4 +85,6 @@ impl LocalModel for ReceivedAuthorizationRequest { } Ok(result) } + + } \ No newline at end of file diff --git a/core/src/data/local/authorization_request/sent.rs b/core/src/data/local/authorization_request/sent.rs index e705693..e5cdf77 100644 --- a/core/src/data/local/authorization_request/sent.rs +++ b/core/src/data/local/authorization_request/sent.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Local, NaiveDateTime}; use iroh::{NodeId, PublicKey}; use rusqlite::types::FromSqlError; -use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; +use crate::{data::local::LocalRecord, global::LOCAL_DATABASE_CONNECTION}; /// Request of node authentication. #[derive(Debug, Clone)] @@ -12,10 +12,10 @@ pub struct SentAuthorizationRequest { public_key: PublicKey, passcode: String, created_at: DateTime, - sent_at: Option>, + responded_at: Option>, } -impl LocalModel for SentAuthorizationRequest { +impl LocalRecord for SentAuthorizationRequest { const TABLE_NAME: &str = "sent_authorization"; const DEFAULT_COLUMNS: &[&str] = &[ @@ -23,17 +23,20 @@ impl LocalModel for SentAuthorizationRequest { "public_key", "passcode", "created_at", - "sent_at" + "responded_at" ]; + type DefaultParams<'a> = (&'a SingleId, &'a [u8;32], &'a str, NaiveDateTime, Option) + where + Self: 'a; fn from_default_row(row: &rusqlite::Row<'_>) -> Result { let created_at: NaiveDateTime = row.get(2)?; - let sent_at: Option = row.get(3)?; + let responded_at: Option = row.get(3)?; Ok(Self { request_id: row.get(0)?, public_key: PublicKey::from_bytes(&row.get(1)?).map_err(|e| FromSqlError::Other(Box::new(e)))?, passcode: row.get(2)?, created_at: DateTime::from(created_at.and_utc()), - sent_at: sent_at.map(|x| DateTime::from(x.and_utc())), + responded_at: responded_at.map(|x| DateTime::from(x.and_utc())), }) } fn insert(&self) -> Result<(), rusqlite::Error> { @@ -45,7 +48,7 @@ impl LocalModel for SentAuthorizationRequest { &self.public_key.as_bytes(), &self.passcode, &self.created_at.naive_utc(), - &self.sent_at.map(|x| x.naive_utc()) + &self.responded_at.map(|x| x.naive_utc()) ), )?; Ok(()) @@ -64,4 +67,8 @@ impl LocalModel for SentAuthorizationRequest { } Ok(result) } + + fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a> { + (&self.request_id, &self.public_key.as_bytes(), &self.passcode, self.created_at.naive_utc(), self.responded_at.map(|x| x.naive_utc())) + } } \ No newline at end of file diff --git a/core/src/data/local/migration/v1.rs b/core/src/data/local/migration/v1.rs index f525d36..11fed69 100644 --- a/core/src/data/local/migration/v1.rs +++ b/core/src/data/local/migration/v1.rs @@ -3,11 +3,10 @@ use rusqlite::{Error, Connection}; pub fn migrate(con: &mut Connection) -> Result<(), Error>{ let tx = con.transaction()?; tx.execute_batch( - "BEGIN; - CREATE TABLE peer ( - id INTEGER PRIMARY KEY, - local_id INTEGER NOT NULL UNIQUE, - public_key BLOB UNIQUE NOT NULL, + "CREATE TABLE peer ( + id INTEGER PRIMARY KEY, + local_peer_id INTEGER NOT NULL UNIQUE, + public_key BLOB UNIQUE NOT NULL ); CREATE TABLE received_authorization_request ( id INTEGER PRIMARY KEY, @@ -20,27 +19,26 @@ pub fn migrate(con: &mut Connection) -> Result<(), Error>{ CREATE TABLE sent_authorization_request ( id INTEGER PRIMARY KEY, request_id INTEGER NOT NULL UNIQUE, - public_key. BLOB NOT NULL UNIQUE, + public_key BLOB NOT NULL UNIQUE, passcode TEXT NOT NULL, created_at TEXT NOT NULL, - sent_at TEXT + responded_at TEXT ); CREATE TABLE authorized_peer ( id INTEGER PRIMARY KEY, node_id BLOB NOT NULL UNIQUE, last_synced_at TEXT, - last_sent_version_vector BLOB + last_sent_version_vector BLOB, created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); CREATE TABLE authorization ( id INTEGER PRIMARY KEY, node_id BLOB UNIQUE NOT NULL, passcode TEXT NOT NULL, created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - ); - COMMIT;", + updated_at TEXT NOT NULL + );", )?; tx.pragma_update(None, "user_version", 1)?; tx.commit()?; diff --git a/core/src/data/local/mod.rs b/core/src/data/local/mod.rs index e7ae821..69c0dce 100644 --- a/core/src/data/local/mod.rs +++ b/core/src/data/local/mod.rs @@ -5,7 +5,7 @@ pub mod migration; use std::{cell::OnceCell, iter::Map, path::Path, sync::{LazyLock, OnceLock}}; use migration::migrate; -use rusqlite::{ffi::Error, Connection, MappedRows, Row}; +use rusqlite::{ffi::Error, params, Connection, MappedRows, OptionalExtension, Params, Row, ToSql}; use crate::{config::StorageConfig, global::{CONFIG, LOCAL_DATABASE_CONNECTION}}; @@ -13,10 +13,63 @@ pub use authorization_request::*; /// Model trait for local database data. /// use LOCAL_DATABASE_CONNECTION for database connection. -pub trait LocalModel: Sized { +pub trait LocalRecord: Sized { const TABLE_NAME: &str; const DEFAULT_COLUMNS: &[&str]; - fn insert(&self) -> Result<(), rusqlite::Error>; + + const DEFAULT_SELECT_STATEMENT: LazyLock = LazyLock::new(|| { + String::from("SELECT ") + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + }); + + const DEFAULT_PLACEHOLDER: LazyLock = LazyLock::new(|| { + let mut result : Vec = Vec::new(); + for i in 0..Self::DEFAULT_COLUMNS.len() { + result.push(String::from("?") + &(i+1).to_string()); + } + result.join(", ") + }); + + type DefaultParams<'a>: Params + where + Self: 'a; + + fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a>; + + fn insert(&self) -> Result<(), rusqlite::Error> { + let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); + + connection.execute( + &("INSERT INTO ".to_owned() + Self::TABLE_NAME + " (" + &Self::DEFAULT_COLUMNS.join(", ") + ") VALUES (" + &*Self::DEFAULT_PLACEHOLDER + ")"), + self.as_default_params() + )?; + Ok(()) + } + + fn get_one_where

(where_statement: &str, params: P) -> Result, rusqlite::Error> + where P: Params + { + let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); + Ok(connection.query_row( + &(String::new() + &Self::DEFAULT_SELECT_STATEMENT + " " + where_statement), + params, + Self::from_default_row + ).optional()?) + } + + fn get_one_by(field_name: &str, field_value: T) -> Result, rusqlite::Error> + where + T: ToSql + { + let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); + Ok(Some(connection.query_row( + &("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + " WHERE " + field_name + "=(?1)"), + params![field_value], + Self::from_default_row + )?)) + } + fn get_one_by_id(id: u32) -> Result, rusqlite::Error> { + Self::get_one_by("id", id ) + } fn from_default_row(row: &Row<'_>) -> Result; fn get_all() -> Result, rusqlite::Error>; } \ No newline at end of file diff --git a/core/src/data/local/peer.rs b/core/src/data/local/peer.rs index 8285e48..53c39ab 100644 --- a/core/src/data/local/peer.rs +++ b/core/src/data/local/peer.rs @@ -8,7 +8,7 @@ use iroh::{NodeId, PublicKey}; use rusqlite::{params, types::FromSqlError, Connection}; use uuid::Uuid; -use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; +use crate::{data::local::{self, LocalRecord}, global::LOCAL_DATABASE_CONNECTION}; /// Peer information cached in local database. /// @@ -17,53 +17,57 @@ use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; /// - Actual peer information is managed by iroh endpoint and not contained in this model. /// - Once a peer is authorized, it is assigned a global (=synced) ID as authorized_peer so essentially this local id targets unauthorized peers. /// -pub struct Peer { - pub local_id: DoubleId, +#[derive(Clone, Debug, PartialEq)] +pub struct PeerRecord { + + /// local id of peer. + /// this id is use only the node itself and not synced so another node has different local_peer_id even if its public_key is same. + pub local_peer_id: DoubleId, pub public_key: PublicKey, } -impl Peer { - pub fn get_by_local_id(local_id: DoubleId) -> Result, rusqlite::Error> { - let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); - Ok(Some(connection.query_row( - &("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + " WHERE local_id=(?1)"), - params![local_id], - Self::from_default_row - )?)) +impl PeerRecord { + pub fn get_or_insert_by_public_key(public_key: &PublicKey) -> Result { + match Self::get_by_public_key(public_key)? { + Some(x) => Ok(x), + None => { + let new = Self{ + local_peer_id: rand::random(), + public_key: public_key.clone(), + }; + new.insert()?; + Ok(new) + } + } + } - pub fn get_by_public_key(public_key: PublicKey) -> Result, rusqlite::Error> { - let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); - Ok(Some(connection.query_row( - &("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + " WHERE public_key=(?1)"), - params![public_key.as_bytes()], - Self::from_default_row - )?)) + pub fn get_by_local_id(local_id: &DoubleId) -> Result, rusqlite::Error> { + Self::get_one_where("WHERE local_peer_id = ?1", (local_id,)) + } + pub fn get_by_public_key(public_key: &PublicKey) -> Result, rusqlite::Error> { + Self::get_one_where("WHERE public_Key = ?1", (public_key.as_bytes(),)) } } -impl LocalModel for Peer { +impl LocalRecord for PeerRecord { const TABLE_NAME: &str = "peer"; const DEFAULT_COLUMNS: &[&str] = &[ - "local_id", + "local_peer_id", "public_key" ]; + type DefaultParams<'a> = (&'a DoubleId, &'a [u8;32]); + fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a> + { + (&self.local_peer_id, &self.public_key.as_bytes()) + } fn from_default_row(row: &rusqlite::Row<'_>) -> Result { - Ok(Self { - local_id: row.get(0)?, + local_peer_id: row.get(0)?, public_key: PublicKey::from_bytes(&row.get(1)?).map_err(|e| FromSqlError::Other(Box::new(e)))? }) } - fn insert(&self) -> Result<(), rusqlite::Error> { - let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); - - connection.execute( - &("INSERT INTO ".to_owned() + Self::TABLE_NAME + " (" + &Self::DEFAULT_COLUMNS.join(", ") + ") VALUES (?1, ?2, ?3, ?4)"), - (&self.local_id, &self.public_key.as_bytes()), - )?; - Ok(()) - } + fn get_all() -> Result, rusqlite::Error> { let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); let mut stmt = connection.prepare(&("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME))?; @@ -77,4 +81,24 @@ impl LocalModel for Peer { } Ok(result) } +} + +#[cfg(test)] +mod tests { + use iroh::SecretKey; + + use crate::tests::TEST_CONFIG; + + use super::*; + + #[test] + fn insert_get_peer_record() { + LOCAL_DATABASE_CONNECTION.get_or_init(&TEST_CONFIG.storage.get_local_database_path()); + let key = SecretKey::generate(&mut rand::rngs::OsRng); + let pubkey = key.public(); + let record = PeerRecord::get_or_insert_by_public_key(&pubkey).unwrap(); + assert_eq!(record, PeerRecord::get_by_local_id(&record.local_peer_id).unwrap().unwrap()); + assert_eq!(record, PeerRecord::get_by_public_key(&record.public_key).unwrap().unwrap()); + + } } \ No newline at end of file