diff --git a/Cargo.lock b/Cargo.lock index 8973967..44ef09c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2216,6 +2216,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ + "getrandom", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 9257bb2..4f1722f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,10 @@ async-channel = "1.8.0" base64 = "0.21.0" r2d2_sqlite = "0.21.0" r2d2 = "0.8.10" -uuid = "1.3.0" + +[dependencies.uuid] +version = "1.3.0" +features = ["v4"] [dependencies.apache-avro] version = "0.14.0" diff --git a/resources/templates/actions.html b/resources/templates/actions.html index 73bd315..dc3b270 100644 --- a/resources/templates/actions.html +++ b/resources/templates/actions.html @@ -13,13 +13,13 @@ diff --git a/resources/templates/partials/actions/create_account.html b/resources/templates/partials/actions/create_account.html new file mode 100644 index 0000000..b10250b --- /dev/null +++ b/resources/templates/partials/actions/create_account.html @@ -0,0 +1,13 @@ + +
+ +
+ +{%include "partials/actions/submit_button.html"%} diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 29e8be7..460d719 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -108,7 +108,8 @@ pub fn list_accounts<'a>(conn: &Connection) -> CustomResult> { sec.id AS sec_id, sec.symbol AS sec_symbol, - sec.security_type AS sec_security_type + sec.security_type AS sec_security_type, + sec.name AS sec_name FROM accounts acc JOIN securities sec ON acc.security = sec.id ORDER BY acc.id ASC"; @@ -130,7 +131,8 @@ pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult String { + match self { + SecurityType::Currency => "currency".to_string(), + SecurityType::Bond => "bond".to_string(), + } + } +} + impl SecurityType { fn from_string_unsafe(s: String) -> SecurityType { if s == "currency".to_string() { @@ -46,12 +55,25 @@ pub struct Security { id: i64, security_type: SecurityType, symbol: String, + name: String, } impl Security { pub fn id(&self) -> SecurityId { SecurityId::from_id(self.id) } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn symbol(&self) -> String { + self.symbol.clone() + } + + pub fn security_type(&self) -> SecurityType { + self.security_type.clone() + } } impl Security { @@ -60,6 +82,7 @@ impl Security { id: row.get("sec_id")?, security_type: SecurityType::from_string_unsafe(row.get("sec_security_type")?), symbol: row.get("sec_symbol")?, + name: row.get("sec_name")?, }) } } @@ -82,7 +105,10 @@ pub fn create_security(tx: &Transaction, sec: &NewSecurity) -> CustomResult CustomResult { - let q = "SELECT id AS sec_id, security_type AS sec_security_type, symbol AS sec_symbol + let q = "SELECT id AS sec_id, + security_type AS sec_security_type, + symbol AS sec_symbol, + name AS sec_name FROM securities WHERE id = ?1"; tx.query_row(q, params![id], Security::from_row) @@ -90,7 +116,10 @@ pub fn get_security_by_id(tx: &Transaction, id: i32) -> CustomResult { } pub fn list_securities(conn: &Connection) -> CustomResult> { - let q = "SELECT id AS sec_id, security_type AS sec_security_type, symbol AS sec_symbol + let q = "SELECT id AS sec_id, + security_type AS sec_security_type, + symbol AS sec_symbol, + name AS sec_name FROM securities ORDER BY id ASC"; let mut stmt = conn.prepare(q).map_err(Error::SqlError)?; @@ -166,6 +195,7 @@ mod test_securities { id: 1, security_type: SecurityType::Currency, symbol: "P".to_string(), + name: "piastre".to_string(), }; assert_eq!(1, ret.len()); assert_eq!(&expt, ret.get(0).unwrap()); @@ -175,6 +205,7 @@ mod test_securities { id: 2, security_type: SecurityType::Bond, symbol: "B".to_string(), + name: "bong".to_string(), }; let tx = conn.transaction().unwrap(); create_security(&tx, &sec)?; diff --git a/src/services/_securities.rs b/src/services/_securities.rs index 9583574..fce6d90 100644 --- a/src/services/_securities.rs +++ b/src/services/_securities.rs @@ -1,6 +1,26 @@ +use crate::db::external_id::SecurityId; +use crate::db::securities::{list_securities, Security}; use rusqlite::Connection; +use serde::{Deserialize, Serialize}; -pub struct SecurityDTO {} +#[derive(Serialize, Deserialize)] +pub struct SecurityDTO { + id: SecurityId, + symbol: String, + name: String, + security_type: String, +} + +impl SecurityDTO { + fn from_entity(sec: &Security) -> SecurityDTO { + SecurityDTO { + id: sec.id(), + symbol: sec.symbol(), + name: sec.name().to_string(), + security_type: sec.security_type().to_string(), + } + } +} pub struct SecurityService<'a> { conn: &'a Connection, @@ -12,6 +32,10 @@ impl SecurityService<'_> { } pub fn list_securities(self) -> Vec { - vec![] + list_securities(self.conn) + .unwrap() + .iter() + .map(SecurityDTO::from_entity) + .collect() } } diff --git a/src/services/mod.rs b/src/services/mod.rs index 552dbfb..57d7565 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,6 +1,5 @@ -use rusqlite::Connection; - pub use crate::services::_authentication::SessionService; +pub use crate::services::_securities::SecurityService; pub use crate::services::_users::UserService; mod _authentication; @@ -9,17 +8,3 @@ mod _transactions; mod _users; pub use crate::db::ledger_internal::LedgerInternal; - -pub struct ReadOnlyServiceFactory<'a> { - conn: &'a mut Connection, -} - -impl ReadOnlyServiceFactory<'_> { - pub fn new(conn: &mut Connection) -> ReadOnlyServiceFactory { - ReadOnlyServiceFactory { conn } - } - - pub fn get_user_service(&self) -> UserService { - UserService::new(self.conn) - } -} diff --git a/src/webui/index_views.rs b/src/webui/index_views.rs index 6e5e2e3..b8bb2b7 100644 --- a/src/webui/index_views.rs +++ b/src/webui/index_views.rs @@ -1,4 +1,4 @@ -use crate::webui::services::Services; +use crate::webui::services::ServicesFactory; use crate::webui::templates::MiniJinjaRenderer; use actix_web::{web, HttpRequest, Responder}; @@ -15,7 +15,7 @@ pub struct LoginPayload { pub async fn login( tmpl_env: MiniJinjaRenderer, web::Json(form): web::Json, - service_factory: web::Data, + service_factory: web::Data, ) -> actix_web::Result { let authentication_service = service_factory.get_authentication_service()?; let cookie = authentication_service.create_login_cookie(&form.username); @@ -30,7 +30,7 @@ pub async fn login( #[actix_web::post("/logout")] pub async fn logout( - service_factory: web::Data, + service_factory: web::Data, tmpl_env: MiniJinjaRenderer, ) -> actix_web::Result { let authentication_service = service_factory.get_authentication_service()?; @@ -43,7 +43,7 @@ pub async fn logout( #[actix_web::get("/")] pub async fn index( req: HttpRequest, - service_factory: web::Data, + service_factory: web::Data, tmpl_env: MiniJinjaRenderer, ) -> actix_web::Result { let authentication_service = service_factory.get_authentication_service()?; diff --git a/src/webui/mod.rs b/src/webui/mod.rs index 69130cf..8dec249 100644 --- a/src/webui/mod.rs +++ b/src/webui/mod.rs @@ -18,7 +18,7 @@ use uuid::Uuid; use crate::events::Event; use crate::util::{CustomResult, Error, SQLiteConnectionFactory}; -use crate::webui::services::{creat_db_pool, Services}; +use crate::webui::services::{creat_db_pool, ServicesFactory}; use crate::webui::templates::MiniJinjaRenderer; use crate::webui::worker::{event_processor_loop, EventProcessor}; @@ -80,11 +80,12 @@ pub async fn web_ui_main( ) -> CustomResult<()> { let (event_sender, event_receiver) = async_channel::unbounded::<(Uuid, Event)>(); let pool = creat_db_pool(&conn_fact.path().unwrap()); - let services = Services::new(pool); + let services = ServicesFactory::new(pool); let server = HttpServer::new(move || { let template_reloader = create_auto_reloader(enable_template_autoreload); App::new() + .wrap(Logger::default()) .app_data(web::Data::new(services.clone())) .app_data(web::Data::new(template_reloader)) .app_data(web::Data::new(event_sender.clone())) @@ -92,7 +93,6 @@ pub async fn web_ui_main( .service(create_api_scope()) .service(create_views_scope()) .wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)) - .wrap(Logger::default()) }) .workers(2) .bind(("0.0.0.0", port)) diff --git a/src/webui/services.rs b/src/webui/services.rs index ecc310d..fb90baa 100644 --- a/src/webui/services.rs +++ b/src/webui/services.rs @@ -1,4 +1,4 @@ -use crate::services::{SessionService, UserService}; +use crate::services::{SecurityService, SessionService, UserService}; use actix_web::{error, Result}; use r2d2::PooledConnection; @@ -19,17 +19,17 @@ struct ServicesCache { } #[derive(Clone)] -pub struct Services { +pub struct ServicesFactory { pool: DbPool, cache: ServicesCache, } -impl Services { - pub fn new(pool: DbPool) -> Services { +impl ServicesFactory { + pub fn new(pool: DbPool) -> ServicesFactory { let mut conn = pool.get().unwrap(); let session_service = SessionService::from_conn(&mut conn).unwrap(); let cache = ServicesCache { session_service }; - Services { pool, cache } + ServicesFactory { pool, cache } } pub fn get_connection(&self) -> Result> { @@ -48,4 +48,11 @@ impl Services { ) -> Result { Ok(UserService::new(conn)) } + + pub fn get_security_service<'a>( + &'a self, + conn: &'a PooledConnection, + ) -> Result { + Ok(SecurityService::new(conn)) + } } diff --git a/src/webui/views.rs b/src/webui/views.rs index 0562f96..e3344e1 100644 --- a/src/webui/views.rs +++ b/src/webui/views.rs @@ -1,14 +1,16 @@ use std::collections::HashMap; use actix_web::{error, web, HttpRequest, Responder}; +use actix_web_lab::respond::Html; use log::info; use minijinja::context; use serde::Deserialize; +use uuid::Uuid; use crate::events::Event; -use crate::webui::services::Services; +use crate::webui::services::ServicesFactory; use crate::webui::templates::MiniJinjaRenderer; #[actix_web::get("/actions")] @@ -24,7 +26,7 @@ pub async fn accounts(tmpl_env: MiniJinjaRenderer) -> actix_web::Result, + service_factory: web::Data, ) -> actix_web::Result { let conn = service_factory.get_connection()?; let user_service = service_factory.get_user_service(&conn)?; @@ -40,7 +42,7 @@ pub async fn overview( pub async fn overview_submit_button( tmpl_env: MiniJinjaRenderer, query: web::Query>, -) -> actix_web::Result { +) -> actix_web::Result { let ctx = context! { event_type => query.get("event_type").unwrap_or(&"".to_string()), }; @@ -51,12 +53,18 @@ pub async fn overview_submit_button( pub async fn actions_partials( tmpl_env: MiniJinjaRenderer, query: web::Query>, -) -> actix_web::Result { + service_factory: web::Data, +) -> actix_web::Result { let event_type = query .get("event_type") .unwrap_or(&"404".to_string()) .to_string(); - let ctx = context! {event_type => event_type}; + let conn = service_factory.get_connection()?; + let security_service = service_factory.get_security_service(&conn)?; + let ctx = context! { + event_type => event_type, + securities => security_service.list_securities(), + }; let template_location = format!("partials/actions/{:}.html", event_type); info!("Getting template at: {:?}", template_location); tmpl_env.render(template_location.as_str(), ctx) @@ -66,15 +74,16 @@ pub async fn actions_partials( pub struct CreateEventPayload { event: Event, event_type: String, + initiator: String, } #[actix_web::post("/events/{event_type}")] pub async fn push_event( req: HttpRequest, tmpl_env: MiniJinjaRenderer, - event_channel: web::Data>, + event_channel: web::Data>, web::Json(form): web::Json, -) -> actix_web::Result { +) -> actix_web::Result { let path_event_type = req.match_info().get("event_type").unwrap(); if path_event_type.to_string() != form.event_type { return Err(error::ErrorBadRequest("Inconsistent event type.")); @@ -83,7 +92,7 @@ pub async fn push_event( if validation.is_err() { return Err(error::ErrorBadRequest(validation.err().unwrap())); } - let _ = event_channel.send(form.event).await; + let _ = event_channel.send((Uuid::new_v4(), form.event)).await; let ctx = context! { event_type => "event received.", }; diff --git a/src/webui/worker.rs b/src/webui/worker.rs index 31177d3..7605392 100644 --- a/src/webui/worker.rs +++ b/src/webui/worker.rs @@ -109,7 +109,7 @@ pub mod test_worker { use super::*; use crate::events::CreateNewUser; use crate::migration; - use futures::{future, TryFutureExt}; + use futures::future; use std::sync::Mutex; struct StatefulEventProcessor { @@ -122,13 +122,13 @@ pub mod test_worker { } fn call_count(&self) -> i32 { - let mut m = self.m.lock().unwrap(); + let m = self.m.lock().unwrap(); m.clone() } } impl EventProcessor for StatefulEventProcessor { - fn process_event(&self, conn: &mut Connection, event: Event) -> CustomResult<()> { + fn process_event(&self, _conn: &mut Connection, event: Event) -> CustomResult<()> { let mut m = self.m.lock().unwrap(); *m += 1; get_event_processing_result(&event) @@ -186,7 +186,7 @@ pub mod test_worker { #[tokio::test] async fn test_on_close_channel() { - let mut ep = StatefulEventProcessor::new(); + let ep = StatefulEventProcessor::new(); let (event_sender, event_receiver) = async_channel::unbounded::<(Uuid, Event)>(); event_sender.close(); let r = event_processor_loop(&mut get_connection(), &ep, event_receiver).await; @@ -196,7 +196,7 @@ pub mod test_worker { #[tokio::test] async fn test_basic() -> CustomResult<()> { - let mut ep = StatefulEventProcessor::new(); + let ep = StatefulEventProcessor::new(); let mut conn = get_connection(); let (event_sender, event_receiver) = async_channel::unbounded::<(Uuid, Event)>(); let f1 = send_events(event_sender, get_events()); diff --git a/tools/run-local b/tools/run-local index e4869ce..1fd6909 100755 --- a/tools/run-local +++ b/tools/run-local @@ -1,2 +1,2 @@ #!/usr/bin/env bash -exec cargo run -- wui run --port 9999 --enable-template-autoreload \ No newline at end of file +exec cargo run -- -vvv wui run --port 9999 --enable-template-autoreload \ No newline at end of file