Irvin Blais rock!

This commit is contained in:
didier amyot committed 2023-04-01 14:27:38 -04:00
1 parent f2c6c8d65c
commit 0d0337a6aa
17 files changed
+175 -39

No files matched your search

+25 -1
View File
@@ -4,7 +4,31 @@
{% block body %} {% block body %}
<div>bob le chef</div> <table class="uk-table uk-table-divider uk-table-striped uk-table-hover uk-table-small">
<thead>
<tr>
<th></th>
<th>id</th>
<th>Amount</th>
<th>Account Type</th>
<th>Last Update</th>
<th>Security</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td><a href="/accounts/{{ account.id }}"><span uk-icon="info"></span></a></td>
<td>{{ account.id }}</td>
<td>{{ account.amount }}</td>
<td>{{ account.account_type }}</td>
<td>{{ account.last_update }}</td>
<td>{{ account.security_symbol }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %} {% endblock %}
@@ -7,6 +7,7 @@
<ul class="uk-navbar-nav"> <ul class="uk-navbar-nav">
{% set nav_bar_items = [{'id': 'overview', 'label': 'Overview'}, {% set nav_bar_items = [{'id': 'overview', 'label': 'Overview'},
{'id': 'accounts', 'label': 'Accounts'}, {'id': 'accounts', 'label': 'Accounts'},
{'id': 'securities', 'label': 'Securities'},
{'id': 'actions', 'label': 'Actions'}]%} {'id': 'actions', 'label': 'Actions'}]%}
{% for itm in nav_bar_items %} {% for itm in nav_bar_items %}
<li class="{% if active_navbar_item == itm.id %}uk-active"{% endif %}"><a href="{{ itm.id }}">{{ itm.label }}</a></li> <li class="{% if active_navbar_item == itm.id %}uk-active"{% endif %}"><a href="{{ itm.id }}">{{ itm.label }}</a></li>
@@ -1,7 +1,7 @@
<div class="uk-margin"> <div class="uk-margin">
<label class="uk-form-label"> Security <label class="uk-form-label"> Security
<select class="uk-select"> <select class="uk-select" name="event.CreateNewAccount.securityId">
{% for sec in securities %} {% for sec in securities %}
<option value="{{ sec.id }}">{{ sec.name }} ({{ sec.symbol }}) </option> <option value="{{ sec.id }}">{{ sec.name }} ({{ sec.symbol }}) </option>
{% endfor %} {% endfor %}
@@ -10,4 +10,7 @@
</label> </label>
</div> </div>
<input type="hidden" name="event.CreateNewAccount.userId" value="{{ user.id }}">
{%include "partials/actions/submit_button.html"%} {%include "partials/actions/submit_button.html"%}
@@ -30,4 +30,6 @@
</label> </label>
</div> </div>
<input type="hidden" name="event.CreateNewSecurity.userId" value="{{ user.id }}">
{%include "partials/actions/submit_button.html"%} {%include "partials/actions/submit_button.html"%}
+34
View File
@@ -0,0 +1,34 @@
{% set active_navbar_item = 'securities' %}
{% extends "base.html" %}
{% block body %}
<table class="uk-table uk-table-divider uk-table-striped uk-table-hover uk-table-small">
<thead>
<tr>
<th></th>
<th>Id</th>
<th>Name</th>
<th>Symbol</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for sec in securities %}
<tr>
<td><a href="/securities/{{ sec.id }}"><span uk-icon="info"></span></a></td>
<td>{{ sec.id }}</td>
<td>{{ sec.name }}</td>
<td>{{ sec.symbol }}</td>
<td>{{ sec.security_type }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
+24 -5
View File
@@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
struct SafeId { struct SafeId {
@@ -101,16 +101,35 @@ macro_rules! impl_ids {
self.0.get_id() self.0.get_id()
} }
} }
impl Serialize for $T {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
serializer.serialize_str(self.to_string().as_str())
}
}
impl<'de> Deserialize<'de> for $T {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>{
let s = String::deserialize(deserializer)?;
$T::from_string(&s).ok_or(serde::de::Error::custom("invalid string format"))
}
}
}; };
} }
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[derive(PartialEq, Debug, Clone)]
pub struct AccountId(SafeId); pub struct AccountId(SafeId);
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[derive(PartialEq, Debug, Clone)]
pub struct UserId(SafeId); pub struct UserId(SafeId);
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] #[derive(PartialEq, Debug, Clone)]
pub struct SecurityId(SafeId); pub struct SecurityId(SafeId);
impl_ids!(AccountId, "a"); impl_ids!(AccountId, "a");
@@ -118,7 +137,7 @@ impl_ids!(UserId, "u");
impl_ids!(SecurityId, "s"); impl_ids!(SecurityId, "s");
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum ExternalId { pub enum ExternalId { // todo: perhaps this abstraction is useless.. duno
AccountId(AccountId), AccountId(AccountId),
UserId(UserId), UserId(UserId),
SecurityId(SecurityId), SecurityId(SecurityId),
+1 -1
View File
@@ -38,7 +38,7 @@ fn _user_from_row(row: &Row) -> Result<User> {
}) })
} }
fn get_user_by_id(tx: &Transaction, id: & UserId) -> CustomResult<Option<User>> { pub fn get_user_by_id(tx: &Connection, id: & UserId) -> CustomResult<Option<User>> {
let q = "SELECT id, name, public_key let q = "SELECT id, name, public_key
FROM users FROM users
WHERE id = ?1"; WHERE id = ?1";
+1 -1
View File
@@ -22,7 +22,7 @@ pub mod securities {
} }
pub mod users { pub mod users {
pub use super::_users::{create_user, list_users, get_user_by_name, User}; pub use super::_users::{create_user, list_users, get_user_by_name, get_user_by_id, User};
} }
pub mod accounts { pub mod accounts {
+11 -2
View File
@@ -1,8 +1,9 @@
use rusqlite::{Connection, Transaction}; use rusqlite::{Connection, Transaction};
use crate::db::accounts::{create_checking_account, create_security_emitter_account};
use crate::db::securities::create_security; use crate::db::securities::create_security;
use crate::db::users::create_user; use crate::db::users::create_user;
use crate::events::{CreateNewSecurity, CreateNewUser, Event}; use crate::events::{CreateNewAccount, CreateNewSecurity, CreateNewUser, Event};
use crate::util::{CustomResult, Error}; use crate::util::{CustomResult, Error};
fn process_create_new_security_event( fn process_create_new_security_event(
@@ -10,7 +11,9 @@ fn process_create_new_security_event(
event: CreateNewSecurity, event: CreateNewSecurity,
) -> CustomResult<()> { ) -> CustomResult<()> {
let r = event.to_create_new_security().unwrap(); let r = event.to_create_new_security().unwrap();
create_security(tx, &r)?; let sec = create_security(tx, &r)?;
create_security_emitter_account(&tx, &sec.id(), &event.userId)?;
Ok(()) Ok(())
} }
@@ -19,11 +22,17 @@ fn process_create_new_user_event(tx: &mut Transaction, event: CreateNewUser) ->
Ok(()) Ok(())
} }
fn process_create_new_account_event(tx: &mut Transaction, event: CreateNewAccount) -> CustomResult<()> {
create_checking_account(&tx, &event.securityId, &event.userId)?;
Ok(())
}
pub fn process_event(conn: &mut Connection, event: Event) -> CustomResult<()> { pub fn process_event(conn: &mut Connection, event: Event) -> CustomResult<()> {
let mut tx = conn.transaction().map_err(Error::SqlError)?; let mut tx = conn.transaction().map_err(Error::SqlError)?;
match event { match event {
Event::CreateNewSecurity(e) => process_create_new_security_event(&mut tx, e)?, Event::CreateNewSecurity(e) => process_create_new_security_event(&mut tx, e)?,
Event::CreateNewUser(e) => process_create_new_user_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)?,
}; };
tx.commit().map_err(Error::SqlError) tx.commit().map_err(Error::SqlError)
} }
+9
View File
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db::external_id::{SecurityId, UserId};
use crate::db::securities::{NewSecurity, SecurityType}; use crate::db::securities::{NewSecurity, SecurityType};
@@ -7,6 +8,7 @@ pub struct CreateNewSecurity {
pub security_type: String, pub security_type: String,
pub symbol: String, pub symbol: String,
pub name: String, pub name: String,
pub userId: UserId,
} }
impl CreateNewSecurity { impl CreateNewSecurity {
@@ -36,10 +38,17 @@ pub struct CreateNewUser {
pub public_key: String, pub public_key: String,
} }
#[derive(Deserialize, Serialize, Debug)]
pub struct CreateNewAccount {
pub securityId: SecurityId,
pub userId: UserId,
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub enum Event { pub enum Event {
CreateNewSecurity(CreateNewSecurity), CreateNewSecurity(CreateNewSecurity),
CreateNewUser(CreateNewUser), CreateNewUser(CreateNewUser),
CreateNewAccount(CreateNewAccount),
} }
impl Event { impl Event {
+2 -8
View File
@@ -38,14 +38,8 @@ impl AccountService<'_> {
AccountService{conn} AccountService{conn}
} }
pub fn get_user_accounts(& self, user_id: String) -> CustomResult<Vec<AccountDTO>>{ pub fn get_user_accounts(& self, user_id: &UserId) -> CustomResult<Vec<AccountDTO>>{
let p = UserId::from_string(&user_id); let ret = list_user_accounts(&self.conn, user_id)?;
if p.is_none() {
let msg = format!("{} is an invalid user id.", &user_id);
return Err(Error::UserInputError(msg));
}
let ret = list_user_accounts(&self.conn, &p.unwrap())?;
Ok(ret.iter().map(AccountDTO::from_entity).collect()) Ok(ret.iter().map(AccountDTO::from_entity).collect())
} }
+22 -4
View File
@@ -4,10 +4,13 @@ use actix_web::HttpRequest;
use cookie::time::OffsetDateTime; use cookie::time::OffsetDateTime;
use cookie::{Cookie, CookieJar, Key}; use cookie::{Cookie, CookieJar, Key};
use rusqlite::Connection; use rusqlite::Connection;
use crate::db::external_id::{UserId, Id};
use crate::services::_users::UserDTO;
use crate::services::UserService;
#[derive(Clone)]
pub struct SessionService<'a> { pub struct SessionService<'a> {
key: &'a Key, key: &'a Key,
user_service: UserService<'a>,
} }
impl SessionService<'_> { impl SessionService<'_> {
@@ -18,8 +21,8 @@ impl SessionService<'_> {
Ok(Key::derive_from(&secret)) Ok(Key::derive_from(&secret))
} }
pub fn from_key(key: &Key) -> SessionService { pub fn create<'a>(key: &'a Key, user_service: UserService<'a>) -> SessionService<'a> {
SessionService{key} SessionService{key, user_service}
} }
pub fn create_login_cookie(&self, user_id: &String) -> Cookie { pub fn create_login_cookie(&self, user_id: &String) -> Cookie {
@@ -39,10 +42,25 @@ impl SessionService<'_> {
// todo: this service should not be aware of http requests. // todo: this service should not be aware of http requests.
// basically, it should be easy to migrate of // basically, it should be easy to migrate of
// web framework // web framework
pub fn get_login(&self, req: &HttpRequest) -> Option<String> { pub fn get_login(&self, req: &HttpRequest) -> Option<UserId> {
req.cookie("ledger-user-id") req.cookie("ledger-user-id")
.and_then(|c| self._decrypt_cookie(c)) .and_then(|c| self._decrypt_cookie(c))
.map(|c| c.value().to_string()) .map(|c| c.value().to_string())
.and_then(|s| UserId::from_string(& s))
}
pub fn get_logged_in_user(&self, req: &HttpRequest) -> CustomResult<Option<UserDTO>> {
let ret = req.cookie("ledger-user-id")
.and_then(|c| self._decrypt_cookie(c))
.map(|c| c.value().to_string())
.and_then(|s| UserId::from_string(& s))
.map(|x| self.user_service.get_user(& x));
if ret.is_none() {
Ok(None)
}else {
ret.unwrap()
}
} }
pub fn logout(&self) -> Cookie { pub fn logout(&self) -> Cookie {
+6 -1
View File
@@ -1,8 +1,9 @@
use rusqlite::Connection; use rusqlite::Connection;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use crate::db::external_id::UserId;
use crate::db::users::{get_user_by_name, list_users, User}; use crate::db::users::{get_user_by_id, get_user_by_name, list_users, User};
use crate::util::CustomResult; use crate::util::CustomResult;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@@ -57,6 +58,10 @@ impl UserService<'_> {
Ok(ret.as_ref().map(UserDTO::from_entity)) Ok(ret.as_ref().map(UserDTO::from_entity))
} }
pub fn get_user(&self, id: &UserId) -> CustomResult<Option<UserDTO>> {
let ret = get_user_by_id(self.conn, id)?;
Ok(ret.as_ref().map(UserDTO::from_entity))
}
pub fn list_users(self) -> CustomResult<Page<UserDTO>> { pub fn list_users(self) -> CustomResult<Page<UserDTO>> {
let users = list_users(self.conn)? let users = list_users(self.conn)?
.iter() .iter()
+7 -9
View File
@@ -28,7 +28,7 @@ pub async fn login(
return Ok(tmpl_env.render("partials/index/failure.html", context!{})?.customize()); return Ok(tmpl_env.render("partials/index/failure.html", context!{})?.customize());
} }
let user = maybeUser.unwrap(); let user = maybeUser.unwrap();
let authentication_service = service_factory.get_authentication_service()?; let authentication_service = service_factory.get_authentication_service(&conn)?;
let cookie = authentication_service.create_login_cookie(&user.id); let cookie = authentication_service.create_login_cookie(&user.id);
let ctx = context! { let ctx = context! {
user => & user, user => & user,
@@ -44,7 +44,8 @@ pub async fn logout(
service_factory: web::Data<ServicesFactory>, service_factory: web::Data<ServicesFactory>,
tmpl_env: MiniJinjaRenderer, tmpl_env: MiniJinjaRenderer,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let authentication_service = service_factory.get_authentication_service()?; let conn = service_factory.get_connection()?;
let authentication_service = service_factory.get_authentication_service(&conn)?;
let res = tmpl_env.render("partials/index/form.html", context! {})?; let res = tmpl_env.render("partials/index/form.html", context! {})?;
Ok(res Ok(res
.customize() .customize()
@@ -58,15 +59,12 @@ pub async fn index(
tmpl_env: MiniJinjaRenderer, tmpl_env: MiniJinjaRenderer,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let conn = service_factory.get_connection()?; let conn = service_factory.get_connection()?;
let authentication_service = service_factory.get_authentication_service()?; let authentication_service = service_factory.get_authentication_service(&conn)?;
let user_service = service_factory.get_user_service(&conn)?; let is_logged_in = authentication_service.get_login(&req).is_some();
let userid = authentication_service.get_login(&req);
let username = userid.clone().unwrap_or("".to_string());
let ctx = context! { let ctx = context! {
is_logged_in => userid.is_some(), is_logged_in => is_logged_in,
// user => user_service.get_user_for_authentication(userid.unwrap(), user => authentication_service.get_logged_in_user(&req).unwrap(),
}; };
tmpl_env.render("index.html", ctx) tmpl_env.render("index.html", ctx)
} }
+1
View File
@@ -67,6 +67,7 @@ fn create_views_scope() -> Scope {
.service(index_views::logout) .service(index_views::logout)
.service(views::actions) .service(views::actions)
.service(views::accounts) .service(views::accounts)
.service(views::securities)
.service(views::overview) .service(views::overview)
.service(views::overview_submit_button) .service(views::overview_submit_button)
.service(views::actions_partials) .service(views::actions_partials)
+6 -3
View File
@@ -1,6 +1,6 @@
use crate::services::{SecurityService, SessionService, UserService, AccountService}; use crate::services::{SecurityService, SessionService, UserService, AccountService};
use actix_web::{error, Result}; use actix_web::{error, Error, Result};
use r2d2::PooledConnection; use r2d2::PooledConnection;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
@@ -39,8 +39,11 @@ impl ServicesFactory {
.map_err(|_e| error::ErrorBadRequest("Cannot get DB connection.")) .map_err(|_e| error::ErrorBadRequest("Cannot get DB connection."))
} }
pub fn get_authentication_service(&self) -> Result<SessionService> { pub fn get_authentication_service<'a>(
Ok(SessionService::from_key(& self.cache.session_service_key)) &'a self,
_conn: &'a PooledConnection<SqliteConnectionManager>,) -> Result<SessionService> {
let user_service = self.get_user_service(_conn)?;
Ok(SessionService::create(& self.cache.session_service_key, user_service))
} }
pub fn get_user_service<'a>( pub fn get_user_service<'a>(
+19 -3
View File
@@ -24,14 +24,27 @@ pub async fn accounts(tmpl_env: MiniJinjaRenderer,
req: HttpRequest, req: HttpRequest,
service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> { service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> {
let conn = service_factory.get_connection()?; let conn = service_factory.get_connection()?;
let auth_service = service_factory.get_authentication_service()?; let auth_service = service_factory.get_authentication_service(&conn)?;
let user = auth_service.get_login(&req); let user = auth_service.get_login(&req);
let account_service = service_factory.get_account_service(& conn)?; let account_service = service_factory.get_account_service(& conn)?;
let accounts = account_service.get_user_accounts(user.unwrap()) let accounts = account_service.get_user_accounts(&user.unwrap())
.map_err(actix_web::error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
tmpl_env.render("accounts.html", context!{accounts => accounts}) tmpl_env.render("accounts.html", context!{accounts => accounts})
} }
#[actix_web::get("/securities")]
pub async fn securities(tmpl_env: MiniJinjaRenderer,
service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> {
let conn = service_factory.get_connection()?;
let account_service = service_factory.get_security_service(& conn)?;
let ctx = context!{
securities => account_service.list_securities()
};
tmpl_env.render("securities.html", ctx)
}
#[actix_web::get("/overview")] #[actix_web::get("/overview")]
pub async fn overview( pub async fn overview(
tmpl_env: MiniJinjaRenderer, tmpl_env: MiniJinjaRenderer,
@@ -60,6 +73,7 @@ pub async fn overview_submit_button(
#[actix_web::get("/partials/actions/action-form")] #[actix_web::get("/partials/actions/action-form")]
pub async fn actions_partials( pub async fn actions_partials(
req: HttpRequest,
tmpl_env: MiniJinjaRenderer, tmpl_env: MiniJinjaRenderer,
query: web::Query<HashMap<String, String>>, query: web::Query<HashMap<String, String>>,
service_factory: web::Data<ServicesFactory>, service_factory: web::Data<ServicesFactory>,
@@ -70,9 +84,11 @@ pub async fn actions_partials(
.to_string(); .to_string();
let conn = service_factory.get_connection()?; let conn = service_factory.get_connection()?;
let security_service = service_factory.get_security_service(&conn)?; let security_service = service_factory.get_security_service(&conn)?;
let authentication_service = service_factory.get_authentication_service(&conn)?;
let ctx = context! { let ctx = context! {
event_type => event_type, event_type => event_type,
securities => security_service.list_securities(), securities => security_service.list_securities(),
user => authentication_service.get_logged_in_user(&req).map_err(actix_web::error::ErrorInternalServerError)?,
}; };
let template_location = format!("partials/actions/{:}.html", event_type); let template_location = format!("partials/actions/{:}.html", event_type);
info!("Getting template at: {:?}", template_location); info!("Getting template at: {:?}", template_location);