diff --git a/.gitignore b/.gitignore index 2c085d1..2ffe9c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules target +*.db diff --git a/Cargo.lock b/Cargo.lock index 9ccaff9..5f35049 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,23 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.3", + "once_cell", + "version_check", +] + +[[package]] +name = "anyhow" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" + [[package]] name = "autocfg" version = "1.0.1" @@ -84,6 +101,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fnv" version = "1.0.7" @@ -219,6 +248,18 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] [[package]] name = "headers" @@ -361,6 +402,10 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" name = "kampanja-kontrolada-servilo" version = "0.1.0" dependencies = [ + "anyhow", + "rusqlite", + "tempfile", + "thiserror", "tokio", "uuid", "warp", @@ -378,6 +423,16 @@ version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +[[package]] +name = "libsqlite3-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.5" @@ -558,6 +613,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -687,6 +748,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "rusqlite" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1058,6 +1134,12 @@ dependencies = [ "getrandom 0.2.3", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.3" diff --git a/servilo/Cargo.toml b/servilo/Cargo.toml index 94998d8..89bc321 100644 --- a/servilo/Cargo.toml +++ b/servilo/Cargo.toml @@ -6,6 +6,12 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1", features = ["full"] } -uuid = { version = "0.8.2", features = ["v4"] } -warp = { version = "0.3.1" } +anyhow = { version = "1" } +rusqlite = { version = "0.26" } +thiserror = { version = "1" } +tokio = { version = "1", features = ["full"] } +uuid = { version = "0.8", features = ["v4"] } +warp = { version = "0.3" } + +[dev-dependencies] +tempfile = { version = "3" } diff --git a/servilo/src/aŭtentigo.rs b/servilo/src/aŭtentigo.rs index 4db1564..ccd1a1c 100644 --- a/servilo/src/aŭtentigo.rs +++ b/servilo/src/aŭtentigo.rs @@ -1,3 +1,5 @@ +use crate::datumbazo::Datumbazo; +use rusqlite::OptionalExtension; use std::{ collections::HashMap, convert::Infallible, @@ -30,6 +32,21 @@ impl From<Ĵetono> for String { } } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct UzantIdentigilo(String); + +impl From<&str> for UzantIdentigilo { + fn from(s: &str) -> Self { + UzantIdentigilo(s.to_owned()) + } +} + +impl From for String { + fn from(s: UzantIdentigilo) -> Self { + s.0.clone() + } +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Uzantnomo(String); @@ -45,33 +62,95 @@ impl From for String { } } -pub trait AŭtentigoDB: Clone + Send + Sync { - fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option; +pub trait AŭtentigoDB: Send { + fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)>; fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono; } -#[derive(Clone)] pub struct MemAŭtentigo { - mem: HashMap<Ĵetono, Uzantnomo>, + sesioj: HashMap<Ĵetono, UzantIdentigilo>, + uzantoj: HashMap, + inversa_uzantoj: HashMap, } impl MemAŭtentigo { pub fn new() -> Self { Self { - mem: HashMap::new(), + sesioj: HashMap::new(), + uzantoj: HashMap::new(), + inversa_uzantoj: HashMap::new(), } } } impl AŭtentigoDB for MemAŭtentigo { - fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option { - self.mem.get(&ĵetono).cloned() + fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)> { + let identigilo = self.sesioj.get(&ĵetono).cloned()?; + let uzantnomo = self.uzantoj.get(&identigilo).cloned()?; + Some((identigilo, uzantnomo)) + } + + fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono { + let identigilo = self.inversa_uzantoj.get(&uzantnomo).cloned().unwrap(); + let ĵetono = Ĵetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + self.sesioj.insert(ĵetono.clone(), identigilo); + ĵetono + } +} + +pub struct DBAŭtentigo { + pool: Datumbazo, +} + +impl DBAŭtentigo { + pub fn kreu(pool: Datumbazo) -> Self { + DBAŭtentigo { pool } + } +} + +impl AŭtentigoDB for DBAŭtentigo { + fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)> { + let konekto = self.pool.konektu().unwrap(); + konekto + .query_row( + "SELECT uzantoj.id, uzantoj.nomo FROM sesioj INNER JOIN uzantoj on sesioj.uzanto == uzantoj.id WHERE sesioj.id = ?", + [String::from(ĵetono)], + |row| { + let identigilo = row.get("id") + .map(|s: String| UzantIdentigilo::from(s.as_str())).unwrap(); + let nomo = row.get("nomo") + .map(|s: String| Uzantnomo::from(s.as_str())).unwrap(); + Ok((identigilo, nomo)) + }, + ) + .optional() + .unwrap() } fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono { let ĵetono = Ĵetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); - self.mem.insert(ĵetono.clone(), uzantnomo); + let mut konekto = self.pool.konektu().unwrap(); + let tr = konekto.transaction().unwrap(); + let uzanta_id: Option = tr + .query_row( + "SELECT id FROM uzantoj WHERE nomo = ?", + [String::from(uzantnomo.clone())], + |row| row.get("id"), + ) + .optional() + .unwrap(); + match uzanta_id { + None => panic!("uzanto ne ekzistas"), + Some(id) => { + tr.execute( + "INSERT INTO sesioj VALUES(?, ?)", + [String::from(ĵetono.clone()), id], + ) + .unwrap(); + } + } + tr.commit().unwrap(); ĵetono } } @@ -82,8 +161,8 @@ pub struct AŭtentigoPostulas; impl Reject for AŭtentigoPostulas {} pub fn kun_aŭtentigo( - auth_ctx: Arc>, -) -> impl Filter + Clone { + auth_ctx: Arc>, +) -> impl Filter + Clone { let auth_ctx = auth_ctx.clone(); warp::header("authentication").and_then({ let auth_ctx = auth_ctx.clone(); @@ -91,7 +170,7 @@ pub fn kun_aŭtentigo( let auth_ctx = auth_ctx.clone(); async move { match auth_ctx.read().unwrap().aŭtentigu(text) { - Some(uzantnomo) => Ok(uzantnomo), + Some(salutiloj) => Ok(salutiloj), None => Err(reject::custom(AŭtentigoPostulas)), } } diff --git a/servilo/src/datumbazo.rs b/servilo/src/datumbazo.rs new file mode 100644 index 0000000..7386364 --- /dev/null +++ b/servilo/src/datumbazo.rs @@ -0,0 +1,116 @@ +use rusqlite::{params, Connection}; +use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +pub struct ManagedConnection<'a> { + pool: &'a Datumbazo, + konekto: Option, +} + +#[derive(Clone)] +pub struct Datumbazo { + dosierindiko: PathBuf, + pool: Arc>>, +} + +impl Datumbazo { + pub fn kreu(dosierindiko: PathBuf) -> Result { + let mut konekto = Connection::open(dosierindiko.clone())?; + + let tx = konekto.transaction()?; + let versio: i32 = tx.pragma_query_value(None, "user_version", |r| r.get(0))?; + println!("versio: {}", versio); + if versio == 0 { + tx.execute_batch( + "CREATE TABLE uzantoj (id string primary key, nomo text); + CREATE TABLE rajtoj (uzanto string, rajto string, foreign key(uzanto) references uzanto(id)); + CREATE TABLE sesioj (id string primary key not null, uzanto string, foreign key(uzanto) references uzantoj(id)); + PRAGMA user_version = 1;", + )?; + } + let versio: i32 = tx.pragma_query_value(None, "user_version", |r| r.get(0))?; + println!("versio: {}", versio); + + tx.commit()?; + + Ok(Datumbazo { + dosierindiko, + pool: Arc::new(Mutex::new(vec![konekto])), + }) + } + + pub fn konektu<'a>(&'a self) -> Result, anyhow::Error> { + let mut pool = self.pool.lock().unwrap(); + match pool.pop() { + Some(konekto) => Ok(ManagedConnection { + pool: &self, + konekto: Some(konekto), + }), + None => { + let konekto = Connection::open(self.dosierindiko.clone())?; + Ok(ManagedConnection { + pool: &self, + konekto: Some(konekto), + }) + } + } + } + + pub fn revenu(&self, konekto: Connection) { + let mut pool = self.pool.lock().unwrap(); + pool.push(konekto); + } +} + +impl Deref for ManagedConnection<'_> { + type Target = Connection; + + fn deref(&self) -> &Connection { + self.konekto.as_ref().unwrap() + } +} + +impl DerefMut for ManagedConnection<'_> { + fn deref_mut(&mut self) -> &mut Connection { + self.konekto.as_mut().unwrap() + } +} + +impl Drop for ManagedConnection<'_> { + fn drop(&mut self) { + self.pool.revenu(self.konekto.take().unwrap()); + } +} + +#[cfg(test)] +mod test { + use super::*; + use tempfile::NamedTempFile; + + #[test] + fn povas_krei_uzanton() { + let vojo = NamedTempFile::new().unwrap().into_temp_path(); + let datumbazo = Datumbazo::kreu(vojo.to_path_buf()).unwrap(); + let mut konekto = datumbazo.konektu().unwrap(); + let tr = konekto.transaction().unwrap(); + tr.execute( + "INSERT INTO uzantoj VALUES(?, ?)", + params![1, String::from("mia-uzantnomo")], + ) + .unwrap(); + tr.commit().unwrap(); + + let konekto = datumbazo.konektu().unwrap(); + let id: Option = konekto + .query_row( + "SELECT id FROM uzantoj WHERE nomo = ?", + [String::from("mia-uzantnomo")], + |row| row.get("id"), + ) + .unwrap(); + assert_eq!(id, Some(1)); + } +} diff --git a/servilo/src/main.rs b/servilo/src/main.rs index 47abd17..c484e6e 100644 --- a/servilo/src/main.rs +++ b/servilo/src/main.rs @@ -1,21 +1,27 @@ use std::convert::Infallible; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; use std::sync::{Arc, RwLock}; use warp::{Filter, Rejection}; #[path = "aŭtentigo.rs"] mod aŭtentigo; -use aŭtentigo::{kun_aŭtentigo, AŭtentigoDB, AŭtentigoPostulas, MemAŭtentigo, Uzantnomo}; +use aŭtentigo::{kun_aŭtentigo, AŭtentigoDB, AŭtentigoPostulas, Uzantnomo}; + +mod datumbazo; +use datumbazo::Datumbazo; -use crate::rajtigo::{MemRajtigo, Rajtigo, Rajto}; mod rajtigo; +use rajtigo::{DBRajtigo, Rajtigo, Rajto}; + +use crate::aŭtentigo::{DBAŭtentigo, UzantIdentigilo}; async fn traktilo_de_listigu_ludantojn( - uzantnomo: Uzantnomo, + identigilo: UzantIdentigilo, rajtigo: Arc>, ) -> Result { - if rajtigo.read().unwrap().havas_rajton(uzantnomo, |rajtoj| { - rajtoj.fold(false, |acc, r| acc || *r == Rajto::from("admin")) + if rajtigo.read().unwrap().havas_rajton(identigilo, |rajtoj| { + rajtoj.fold(false, |acc, r| acc || r == Rajto::from("admin")) }) { Ok(warp::reply::json(&vec!["Alice", "Betty", "Charles"])) } else { @@ -42,17 +48,15 @@ async fn traktilu_erarojn(err: Rejection) -> Result for String { } pub trait Rajtigo { - fn havas_rajton<'a, F>(&'a self, uzantnomo: Uzantnomo, provo: F) -> bool + fn havas_rajton<'a, F>(&'a self, identigilo: UzantIdentigilo, provo: F) -> bool where - F: FnOnce(&mut (dyn Iterator + 'a)) -> bool; - fn rajtoj(&self, uzantnomo: &Uzantnomo) -> Vec; - fn aldonu_rajtoj(&mut self, uzantnomo: Uzantnomo, rajtoj: &mut (dyn Iterator)); - fn foriru_rajtoj(&mut self, uzantnomo: Uzantnomo, rajtoj: &mut (dyn Iterator)); - fn foriru_uzanto(&mut self, uzantnomo: Uzantnomo); + F: FnOnce(&mut (dyn Iterator)) -> bool; + fn rajtoj(&self, identigilo: &UzantIdentigilo) -> Vec; + fn aldonu_rajtoj( + &mut self, + identigilo: UzantIdentigilo, + rajtoj: &mut (dyn Iterator), + ); + fn foriru_rajtoj( + &mut self, + identigilo: UzantIdentigilo, + rajtoj: &mut (dyn Iterator), + ); + fn foriru_uzanto(&mut self, identigilo: UzantIdentigilo); } +/* pub struct MemRajtigo { - rajtoj: HashMap>, +rajtoj: HashMap>, } impl MemRajtigo { - pub fn new() -> Self { - MemRajtigo { - rajtoj: HashMap::new(), - } - } +pub fn new() -> Self { +MemRajtigo { +rajtoj: HashMap::new(), +} +} } impl Rajtigo for MemRajtigo { - fn havas_rajton<'a, F>(&'a self, uzantnomo: Uzantnomo, provo: F) -> bool - where - F: FnOnce(&mut (dyn Iterator + 'a)) -> bool, - { - match self.rajtoj.get(&uzantnomo) { - Some(rajtoj) => provo(&mut rajtoj.iter()), - None => false, - } - } - fn rajtoj(&self, uzantnomo: &Uzantnomo) -> Vec { - self.rajtoj - .get(&uzantnomo) - .map(|r| r.into_iter().cloned().collect::>()) - .unwrap_or(vec![]) - } - fn aldonu_rajtoj(&mut self, uzantnomo: Uzantnomo, rajtoj: &mut (dyn Iterator)) { - let valoro = self.rajtoj.entry(uzantnomo).or_insert(HashSet::new()); +fn havas_rajton<'a, F>(&'a self, identigilo: UzantIdentigilo, provo: F) -> bool +where +F: FnOnce(&mut (dyn Iterator + 'a)) -> bool, +{ +match self.rajtoj.get(&identigilo) { +Some(rajtoj) => provo(&mut rajtoj.clone().into_iter()), +None => false, +} +} +fn rajtoj(&self, identigilo: &UzantIdentigilo) -> Vec { +self.rajtoj +.get(&identigilo) +.map(|r| r.into_iter().cloned().collect::>()) +.unwrap_or(vec![]) +} +fn aldonu_rajtoj( +&mut self, +identigilo: UzantIdentigilo, +rajtoj: &mut (dyn Iterator), +) { +let valoro = self.rajtoj.entry(identigilo).or_insert(HashSet::new()); - while let Some(r) = rajtoj.next() { - valoro.insert(r); - } - } - fn foriru_rajtoj(&mut self, uzantnomo: Uzantnomo, rajtoj: &mut (dyn Iterator)) { - let valoro = self.rajtoj.entry(uzantnomo).or_insert(HashSet::new()); +while let Some(r) = rajtoj.next() { +valoro.insert(r); +} +} +fn foriru_rajtoj( +&mut self, +identigilo: UzantIdentigilo, +rajtoj: &mut (dyn Iterator), +) { +let valoro = self.rajtoj.entry(identigilo).or_insert(HashSet::new()); - while let Some(r) = rajtoj.next() { - valoro.remove(&r); - } - } - fn foriru_uzanto(&mut self, uzantnomo: Uzantnomo) { - let _ = self.rajtoj.remove(&uzantnomo); +while let Some(r) = rajtoj.next() { +valoro.remove(&r); +} +} +fn foriru_uzanto(&mut self, identigilo: UzantIdentigilo) { +let _ = self.rajtoj.remove(&identigilo); +} +} +*/ + +pub struct DBRajtigo { + pool: Datumbazo, +} + +impl DBRajtigo { + pub fn kreu(pool: Datumbazo) -> Self { + DBRajtigo { pool } } } + +impl Rajtigo for DBRajtigo { + fn havas_rajton<'a, F>(&'a self, identigilo: UzantIdentigilo, provo: F) -> bool + where + F: FnOnce(&mut (dyn Iterator)) -> bool, + { + let konekto = self.pool.konektu().unwrap(); + let mut stmt = konekto + .prepare("SELECT rajto FROM rajtoj WHERE uzanto = ?") + .unwrap(); + let mut rajtoj = stmt + .query_map([String::from(identigilo)], |row| { + row.get("rajto").map(|s: String| Rajto::from(s.as_ref())) + }) + .unwrap() + .map(|r| r.unwrap()); + provo(&mut rajtoj) + } + + fn rajtoj(&self, identigilo: &UzantIdentigilo) -> Vec { + vec![] + } + + fn aldonu_rajtoj( + &mut self, + identigilo: UzantIdentigilo, + rajtoj: &mut (dyn Iterator), + ) { + } + + fn foriru_rajtoj( + &mut self, + identigilo: UzantIdentigilo, + rajtoj: &mut (dyn Iterator), + ) { + } + fn foriru_uzanto(&mut self, identigilo: UzantIdentigilo) {} +} diff --git a/shell.nix b/shell.nix index 97fdfb8..4827c10 100644 --- a/shell.nix +++ b/shell.nix @@ -14,6 +14,7 @@ in pkgs.mkShell { pkgs.nodejs pkgs.openssl pkgs.pkg-config + pkgs.sqlite pkgs.wrapGAppsHook rust unstable.rust-analyzer