From f140b50be4bd08fbee4aa0be49adbeeca3ebd0cd Mon Sep 17 00:00:00 2001 From: didier amyot Date: Mon, 3 Apr 2023 11:55:34 -0400 Subject: [PATCH] Going somewhere. --- README.md | 19 ++++-- resources/templates/account.html | 10 ++++ resources/templates/accounts.html | 4 +- resources/templates/actions.html | 3 +- resources/templates/components/navbar.html | 2 +- .../accounts-by-security-selectors.html | 3 + .../partials/actions/emit_securities.html | 42 ++++++++++++++ resources/templates/security.html | 10 ++++ src/db/_accounts.rs | 25 ++++++++ src/db/_external_id.rs | 3 +- src/db/mod.rs | 2 +- src/event_processor.rs | 9 ++- src/events.rs | 10 +++- src/services/_accounts.rs | 24 ++++++-- src/services/_securities.rs | 4 +- src/webui/mod.rs | 3 + src/webui/views.rs | 58 ++++++++++++++++++- 17 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 resources/templates/account.html create mode 100644 resources/templates/partials/actions/accounts-by-security-selectors.html create mode 100644 resources/templates/partials/actions/emit_securities.html create mode 100644 resources/templates/security.html diff --git a/README.md b/README.md index 6c822bd..8fc7ae1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,15 @@ The rust source must be `/nix/store/pmih7jv9r5xfaif92bza8w05876qvnvc-rust-src/` nix develop ``` +## Technologies + +### Front-end + +* htmx +* https://dc-js.github.io/dc.js/ +* https://dygraphs.com/ +* https://github.com/aftertheflood/sparks + ## Todos #### Architecture @@ -35,10 +44,12 @@ nix develop #### Backend * [ ] Parametrize `CURRENT_TIMESTAMP` +* [ ] Make certain the user that creates the event is allowed to do it. +* [ ] sd #### Front end -* [ ] Serve static files using actix -* [ ] Hack https://github.com/bigskysoftware/htmx/blob/master/src/ext/json-enc.js to all embedded fields. -* [ ] Create UI to create user -* [ ] Create UI to create security +* [x] Serve static files using actix +* [x] Hack https://github.com/bigskysoftware/htmx/blob/master/src/ext/json-enc.js to all embedded fields. +* [x] Create UI to create user +* [x] Create UI to create security diff --git a/resources/templates/account.html b/resources/templates/account.html new file mode 100644 index 0000000..27670f8 --- /dev/null +++ b/resources/templates/account.html @@ -0,0 +1,10 @@ +{% set active_navbar_item = 'accounts' %} + +{% extends "base.html" %} + +{% block body %} + +

Account!

+ + +{% endblock %} \ No newline at end of file diff --git a/resources/templates/accounts.html b/resources/templates/accounts.html index 5dbabf9..5f5feb1 100644 --- a/resources/templates/accounts.html +++ b/resources/templates/accounts.html @@ -22,9 +22,9 @@ {{ account.id }} {{ account.amount }} - {{ account.account_type }} + {{ account.account_type | replace("_", " ") | lower}} {{ account.last_update }} - {{ account.security_symbol }} + {{ account.security.symbol }} {% endfor %} diff --git a/resources/templates/actions.html b/resources/templates/actions.html index dc3b270..7793d9e 100644 --- a/resources/templates/actions.html +++ b/resources/templates/actions.html @@ -16,10 +16,11 @@ hx-get="/partials/actions/action-form" hx-trigger="change,load" hx-target="#action-form" - hx-swap="innerHTML">> + hx-swap="innerHTML"> + diff --git a/resources/templates/components/navbar.html b/resources/templates/components/navbar.html index 065bf28..ce1d57a 100644 --- a/resources/templates/components/navbar.html +++ b/resources/templates/components/navbar.html @@ -10,7 +10,7 @@ {'id': 'securities', 'label': 'Securities'}, {'id': 'actions', 'label': 'Actions'}]%} {% for itm in nav_bar_items %} -
  • {{ itm.label }}
  • +
  • {{ itm.label }}
  • {% endfor %} diff --git a/resources/templates/partials/actions/accounts-by-security-selectors.html b/resources/templates/partials/actions/accounts-by-security-selectors.html new file mode 100644 index 0000000..795b00e --- /dev/null +++ b/resources/templates/partials/actions/accounts-by-security-selectors.html @@ -0,0 +1,3 @@ +{% for account in accounts %} + +{% endfor %} \ No newline at end of file diff --git a/resources/templates/partials/actions/emit_securities.html b/resources/templates/partials/actions/emit_securities.html new file mode 100644 index 0000000..10daa07 --- /dev/null +++ b/resources/templates/partials/actions/emit_securities.html @@ -0,0 +1,42 @@ +
    + +
    + + +
    + +
    + +
    + +
    + +{%include "partials/actions/submit_button.html"%} diff --git a/resources/templates/security.html b/resources/templates/security.html new file mode 100644 index 0000000..a9eb4d9 --- /dev/null +++ b/resources/templates/security.html @@ -0,0 +1,10 @@ +{% set active_navbar_item = 'securities' %} + +{% extends "base.html" %} + +{% block body %} + +

    Security!

    + + +{% endblock %} \ No newline at end of file diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 676b483..898875c 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -195,6 +195,31 @@ pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult CustomResult> { + let q = " + SELECT acc.id AS acc_id, + acc.amount AS acc_amount, + UNIXEPOCH(acc.timestamp) AS acc_timestamp, + account_type AS acc_account_type, + + sec.id AS sec_id, + sec.symbol AS sec_symbol, + sec.security_type AS sec_security_type, + sec.name AS sec_name + FROM accounts acc + JOIN securities sec ON acc.security = sec.id + WHERE sec.id = ?1"; + let mut stmt = tx.prepare(q).map_err(Error::SqlError)?; + let ret: Vec = stmt + .query_map(params![&sec_id], &Account::from_row) + .map_err(Error::SqlError)? + .filter(Result::is_ok) + .map(Result::unwrap) + .collect(); + return Ok(ret); + +} + fn create_account( tx: &Connection, sec: &SecurityId, diff --git a/src/db/_external_id.rs b/src/db/_external_id.rs index 4804da5..aabb68e 100644 --- a/src/db/_external_id.rs +++ b/src/db/_external_id.rs @@ -125,7 +125,6 @@ macro_rules! impl_ids { #[derive(PartialEq, Debug, Clone)] pub struct AccountId(SafeId); - #[derive(PartialEq, Debug, Clone)] pub struct UserId(SafeId); @@ -137,7 +136,7 @@ impl_ids!(UserId, "u"); impl_ids!(SecurityId, "s"); #[derive(PartialEq, Debug)] -pub enum ExternalId { // todo: perhaps this abstraction is useless.. duno +enum ExternalId { // todo: perhaps this abstraction is useless.. duno AccountId(AccountId), UserId(UserId), SecurityId(SecurityId), diff --git a/src/db/mod.rs b/src/db/mod.rs index be0f22d..7edfb4d 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -29,7 +29,7 @@ pub mod accounts { pub use super::_accounts::{ add_to_account, create_checking_account, create_security_emitter_account, get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt, - list_user_accounts + list_user_accounts, AccountType, get_checking_accounts_for_sec_id }; } diff --git a/src/event_processor.rs b/src/event_processor.rs index 8dfa67b..e0e1333 100644 --- a/src/event_processor.rs +++ b/src/event_processor.rs @@ -3,7 +3,7 @@ use crate::db::accounts::{create_checking_account, create_security_emitter_accou use crate::db::securities::create_security; use crate::db::users::create_user; -use crate::events::{CreateNewAccount, CreateNewSecurity, CreateNewUser, Event}; +use crate::events::{CreateNewAccount, CreateNewSecurity, CreateNewUser, EmitSecurities, Event}; use crate::util::{CustomResult, Error}; fn process_create_new_security_event( @@ -27,12 +27,19 @@ fn process_create_new_account_event(tx: &mut Transaction, event: CreateNewAccoun Ok(()) } +fn process_emit_securities_event(tx: &mut Transaction, event: EmitSecurities) -> CustomResult<()> { + // create_checking_account(&tx, &event.securityId, &event.userId)?; + println!("Here it goes > {:?}", event); + Ok(()) +} + pub fn process_event(conn: &mut Connection, event: Event) -> CustomResult<()> { let mut tx = conn.transaction().map_err(Error::SqlError)?; match event { Event::CreateNewSecurity(e) => process_create_new_security_event(&mut tx, e)?, Event::CreateNewUser(e) => process_create_new_user_event(&mut tx, e)?, Event::CreateNewAccount(e) => process_create_new_account_event(&mut tx, e)?, + Event::EmitSecurities(e) => process_emit_securities_event(&mut tx, e)?, }; tx.commit().map_err(Error::SqlError) } diff --git a/src/events.rs b/src/events.rs index 9a09f1d..f8a1111 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::db::external_id::{SecurityId, UserId}; +use crate::db::external_id::{AccountId, SecurityId, UserId}; use crate::db::securities::{NewSecurity, SecurityType}; @@ -44,11 +44,19 @@ pub struct CreateNewAccount { pub userId: UserId, } +#[derive(Deserialize, Serialize, Debug)] +pub struct EmitSecurities { + pub amount: i64, + pub emitter_account: AccountId, + pub payee_accounts: Vec, +} + #[derive(Deserialize, Serialize, Debug)] pub enum Event { CreateNewSecurity(CreateNewSecurity), CreateNewUser(CreateNewUser), CreateNewAccount(CreateNewAccount), + EmitSecurities(EmitSecurities), } impl Event { diff --git a/src/services/_accounts.rs b/src/services/_accounts.rs index b97fcac..d9fa84f 100644 --- a/src/services/_accounts.rs +++ b/src/services/_accounts.rs @@ -1,9 +1,12 @@ use rusqlite::Connection; -use crate::db::accounts::{Account, list_user_accounts}; -use crate::db::external_id::{Id, UserId}; +use crate::db::accounts::{Account, AccountType, get_checking_accounts_for_sec_id, list_user_accounts}; +use crate::db::external_id::{Id, SecurityId, UserId}; use crate::util::{CustomResult, Error}; use serde::Deserialize; use serde::Serialize; +use crate::db::securities::Security; +use crate::services::_securities::SecurityDTO; + pub struct AccountService<'a> { conn: &'a Connection, } @@ -14,7 +17,7 @@ pub struct AccountDTO { pub amount: i64, pub account_type: String, pub last_update: i64, - pub security_symbol: String, + pub security: SecurityDTO, } impl AccountDTO { @@ -24,7 +27,7 @@ impl AccountDTO { amount: account.amount(), account_type: account.account_type().to_string(), last_update: account.timestamp(), - security_symbol: account.security().symbol() + security: SecurityDTO::from_entity(account.security()) } } } @@ -43,4 +46,17 @@ impl AccountService<'_> { Ok(ret.iter().map(AccountDTO::from_entity).collect()) } + pub fn get_user_emitter_accounts(& self, user_id: &UserId) -> CustomResult> { + let accounts = self.get_user_accounts(user_id)?; + let ret = accounts.into_iter() + .filter(|acc| acc.account_type == AccountType::SecurityEmitter.to_string()) + .collect(); + Ok(ret) + } + + pub fn get_checking_accounts_for_security(& self, sec_id: & SecurityId) -> CustomResult>{ + let ret = get_checking_accounts_for_sec_id(self.conn, sec_id)?; + Ok(ret.iter().map(AccountDTO::from_entity).collect()) + + } } \ No newline at end of file diff --git a/src/services/_securities.rs b/src/services/_securities.rs index fce6d90..849fc10 100644 --- a/src/services/_securities.rs +++ b/src/services/_securities.rs @@ -3,7 +3,7 @@ use crate::db::securities::{list_securities, Security}; use rusqlite::Connection; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct SecurityDTO { id: SecurityId, symbol: String, @@ -12,7 +12,7 @@ pub struct SecurityDTO { } impl SecurityDTO { - fn from_entity(sec: &Security) -> SecurityDTO { + pub fn from_entity(sec: &Security) -> SecurityDTO { SecurityDTO { id: sec.id(), symbol: sec.symbol(), diff --git a/src/webui/mod.rs b/src/webui/mod.rs index 08c34ec..b87024f 100644 --- a/src/webui/mod.rs +++ b/src/webui/mod.rs @@ -67,10 +67,13 @@ fn create_views_scope() -> Scope { .service(index_views::logout) .service(views::actions) .service(views::accounts) + .service(views::account) .service(views::securities) + .service(views::security) .service(views::overview) .service(views::overview_submit_button) .service(views::actions_partials) + .service(views::get_accounts_by_security_selectors) } #[tokio::main] diff --git a/src/webui/views.rs b/src/webui/views.rs index 16a8216..fee1ee2 100644 --- a/src/webui/views.rs +++ b/src/webui/views.rs @@ -8,6 +8,7 @@ use minijinja::context; use rusqlite::params; use serde::Deserialize; use uuid::Uuid; +use crate::db::external_id::{Id, SecurityId}; use crate::events::Event; @@ -32,6 +33,14 @@ pub async fn accounts(tmpl_env: MiniJinjaRenderer, tmpl_env.render("accounts.html", context!{accounts => accounts}) } +#[actix_web::get("/accounts/{account_id}")] +pub async fn account(tmpl_env: MiniJinjaRenderer, + req: HttpRequest, + service_factory: web::Data) -> actix_web::Result { + + tmpl_env.render("account.html", context!{}) +} + #[actix_web::get("/securities")] @@ -45,6 +54,14 @@ pub async fn securities(tmpl_env: MiniJinjaRenderer, tmpl_env.render("securities.html", ctx) } +#[actix_web::get("/securities/{sec_id}")] +pub async fn security(tmpl_env: MiniJinjaRenderer, + service_factory: web::Data) -> actix_web::Result { + + let ctx = context!{}; + tmpl_env.render("security.html", ctx) +} + #[actix_web::get("/overview")] pub async fn overview( tmpl_env: MiniJinjaRenderer, @@ -71,6 +88,37 @@ pub async fn overview_submit_button( tmpl_env.render("partials/overview/submit_button.html", ctx) } + +#[actix_web::get("/partials/actions/accounts-by-security-selectors")] +pub async fn get_accounts_by_security_selectors( + req: HttpRequest, + tmpl_env: MiniJinjaRenderer, + query: web::Query>, + service_factory: web::Data, +) -> actix_web::Result { + let sec_id = query + .get("sec-id") + .and_then(SecurityId::from_string) + .ok_or(error::ErrorBadRequest("Invalid sec-id parameters"))?; + let conn = service_factory.get_connection()?; + let account_service = service_factory.get_account_service(&conn)?; + let authentication_service = service_factory.get_authentication_service(&conn)?; + + let maybe_user = authentication_service.get_login(&req); + if maybe_user.is_none() { + return Err(error::ErrorForbidden("")) + } + let user_id = maybe_user.unwrap(); + + let ctx = context! { + sec_id => &sec_id, + user_id => &user_id, + accounts => account_service.get_checking_accounts_for_security(&sec_id).map_err(error::ErrorInternalServerError)?, + }; + tmpl_env.render("partials/actions/accounts-by-security-selectors.html", ctx) +} + + #[actix_web::get("/partials/actions/action-form")] pub async fn actions_partials( req: HttpRequest, @@ -84,11 +132,19 @@ pub async fn actions_partials( .to_string(); let conn = service_factory.get_connection()?; let security_service = service_factory.get_security_service(&conn)?; + let account_service = service_factory.get_account_service(&conn)?; let authentication_service = service_factory.get_authentication_service(&conn)?; + let maybeUser = authentication_service.get_login(&req); + if maybeUser.is_none() { + return Err(actix_web::error::ErrorForbidden("")) + } + let user = maybeUser.unwrap(); let ctx = context! { event_type => event_type, securities => security_service.list_securities(), - user => authentication_service.get_logged_in_user(&req).map_err(actix_web::error::ErrorInternalServerError)?, + emitter_accounts => account_service.get_user_emitter_accounts(&user).map_err(error::ErrorInternalServerError)?, + accounts => account_service.get_user_emitter_accounts(&user).map_err(error::ErrorInternalServerError)?, + user => authentication_service.get_logged_in_user(&req).map_err(error::ErrorInternalServerError)?, }; let template_location = format!("partials/actions/{:}.html", event_type); info!("Getting template at: {:?}", template_location);