Added trivial web ui.
This commit is contained in:
1 parent
f99513a8c8
commit
894f969d0f
11 files changed
+1382
-19
No files matched your search
+6
-6
@@ -190,16 +190,16 @@ pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> Cu
|
||||
Ok(option.is_some())
|
||||
}
|
||||
|
||||
pub struct Receipt {
|
||||
pub struct AccountChangeReceipt {
|
||||
pub account_id: AccountId,
|
||||
pub version: i64,
|
||||
pub timestamp: i64,
|
||||
pub amount: i64,
|
||||
}
|
||||
|
||||
impl Receipt {
|
||||
pub fn from_row(row: &Row) -> Result<Receipt> {
|
||||
Ok(Receipt {
|
||||
impl AccountChangeReceipt {
|
||||
pub fn from_row(row: &Row) -> Result<AccountChangeReceipt> {
|
||||
Ok(AccountChangeReceipt {
|
||||
account_id: AccountId::from_id(row.get("id")?),
|
||||
version: row.get("version")?,
|
||||
amount: row.get("amount")?,
|
||||
@@ -224,7 +224,7 @@ fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResu
|
||||
|
||||
pub fn add_to_account(tx: &Connection,
|
||||
account_id: &AccountId,
|
||||
amount: i64) -> CustomResult<Receipt> {
|
||||
amount: i64) -> CustomResult<AccountChangeReceipt> {
|
||||
let q = "
|
||||
UPDATE accounts SET amount = CASE account_type
|
||||
WHEN 'security_emitter'
|
||||
@@ -236,7 +236,7 @@ pub fn add_to_account(tx: &Connection,
|
||||
RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp
|
||||
";
|
||||
let params = params![amount, account_id];
|
||||
let ret = tx.query_row(q, params, Receipt::from_row).map_err(Error::SqlError);
|
||||
let ret = tx.query_row(q, params, AccountChangeReceipt::from_row).map_err(Error::SqlError);
|
||||
update_account_history(tx, account_id)?;
|
||||
return ret;
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ pub mod users {
|
||||
|
||||
pub mod accounts {
|
||||
pub use super::_accounts::{Account,
|
||||
Receipt,
|
||||
AccountChangeReceipt,
|
||||
create_checking_account,
|
||||
create_security_emitter_account,
|
||||
is_owner,
|
||||
|
||||
+80
-3
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
|
||||
use clap::parser::ValuesRef;
|
||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||
use log::{debug, error, info};
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
@@ -10,14 +10,16 @@ use simple_logger::SimpleLogger;
|
||||
use db::securities::list_securities;
|
||||
|
||||
use crate::db::users::{create_user, list_users};
|
||||
use crate::event_processor::{process_event_files};
|
||||
use crate::event_processor::process_event_files;
|
||||
use crate::migration::apply_migrations;
|
||||
use crate::webui::web_ui_main;
|
||||
|
||||
mod db;
|
||||
mod event_processor;
|
||||
mod migration;
|
||||
mod services;
|
||||
mod util;
|
||||
mod webui;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Output<T> {
|
||||
@@ -42,6 +44,8 @@ trait CommandLineUserLogic {
|
||||
fn list_securities(&mut self, conn: &Connection);
|
||||
|
||||
fn process_events(&mut self, conn: &mut Connection, files: ValuesRef<PathBuf>);
|
||||
|
||||
fn run_webui(&mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool);
|
||||
}
|
||||
|
||||
fn verbosity_arg() -> Arg {
|
||||
@@ -130,12 +134,36 @@ fn ls_securities_command_line_parser() -> Command {
|
||||
.about("List all securities.");
|
||||
}
|
||||
|
||||
fn run_web_ui_command_line_parser() -> Command {
|
||||
return Command::new("run")
|
||||
.arg(Arg::new("port")
|
||||
.long("port")
|
||||
.value_parser(clap::value_parser!(u16).range(3000..))
|
||||
.action(ArgAction::Set)
|
||||
.default_value("3200"))
|
||||
.arg(Arg::new("enable-template-autoreload")
|
||||
.long("enable-template-autoreload")
|
||||
.required(false)
|
||||
.num_args(0)
|
||||
.help("Reload underlying template (for development)")
|
||||
)
|
||||
.about("Run web user interface.");
|
||||
}
|
||||
|
||||
fn securities_command_line_parser() -> Command {
|
||||
return Command::new("securities")
|
||||
.about("Handy command about securities.")
|
||||
.subcommand(ls_securities_command_line_parser())
|
||||
.arg_required_else_help(true);
|
||||
}
|
||||
|
||||
fn web_ui_command_line_parser() -> Command {
|
||||
return Command::new("wui")
|
||||
.about("Web user interface.")
|
||||
.subcommand(run_web_ui_command_line_parser())
|
||||
.arg_required_else_help(true);
|
||||
}
|
||||
|
||||
fn command_line_parser() -> Command {
|
||||
return Command::new("prog")
|
||||
.arg(verbosity_arg())
|
||||
@@ -143,6 +171,7 @@ fn command_line_parser() -> Command {
|
||||
.subcommand(event_command_line_parser())
|
||||
.subcommand(user_command_line_parser())
|
||||
.subcommand(securities_command_line_parser())
|
||||
.subcommand(web_ui_command_line_parser())
|
||||
.arg_required_else_help(true);
|
||||
}
|
||||
|
||||
@@ -207,6 +236,17 @@ fn process_process_events_command_line(
|
||||
logic.process_events(conn, files);
|
||||
}
|
||||
|
||||
fn process_run_wui_command_line(
|
||||
conn: &mut Connection,
|
||||
m: &ArgMatches,
|
||||
logic: &mut dyn CommandLineUserLogic,
|
||||
) {
|
||||
debug!("Processing run wui command line {:?}.", m);
|
||||
let mut port = m.get_one::<u16>("port").unwrap_or(&3200);
|
||||
let enable_template_autoreload = m.get_flag("enable-template-autoreload");
|
||||
logic.run_webui(conn, *port, enable_template_autoreload);
|
||||
}
|
||||
|
||||
fn process_securities_command_line(
|
||||
conn: &Connection,
|
||||
m: &ArgMatches,
|
||||
@@ -235,6 +275,20 @@ fn process_events_command_line(
|
||||
}
|
||||
}
|
||||
|
||||
fn process_wui_command_line(
|
||||
conn: &mut Connection,
|
||||
m: &ArgMatches,
|
||||
logic: &mut dyn CommandLineUserLogic,
|
||||
) {
|
||||
debug!("Processing wui command line {:?}.", m);
|
||||
match m.subcommand() {
|
||||
Some(("run", run_wui_match)) => {
|
||||
process_run_wui_command_line(conn, run_wui_match, logic);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_command_line(
|
||||
conn: &mut Connection,
|
||||
m: &ArgMatches,
|
||||
@@ -245,13 +299,15 @@ fn process_command_line(
|
||||
Some(("users", user_match)) => {
|
||||
process_user_command_line(conn, user_match, logic);
|
||||
}
|
||||
|
||||
Some(("securities", securities_match)) => {
|
||||
process_securities_command_line(conn, securities_match, logic);
|
||||
}
|
||||
Some(("events", events_match)) => {
|
||||
process_events_command_line(conn, events_match, logic);
|
||||
}
|
||||
Some(("wui", wui_match)) => {
|
||||
process_wui_command_line(conn, wui_match, logic);
|
||||
}
|
||||
_ => {
|
||||
println!("Received un parsable command. {:?}", m.subcommand());
|
||||
unreachable!();
|
||||
@@ -268,6 +324,7 @@ mod cli_test {
|
||||
create_user_count: usize,
|
||||
list_securities_count: usize,
|
||||
process_events_count: usize,
|
||||
run_webui_count: usize,
|
||||
}
|
||||
|
||||
impl TestCommandLineLogic {
|
||||
@@ -277,6 +334,7 @@ mod cli_test {
|
||||
create_user_count: 0,
|
||||
list_securities_count: 0,
|
||||
process_events_count: 0,
|
||||
run_webui_count: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -297,6 +355,10 @@ mod cli_test {
|
||||
fn process_events(&mut self, conn: &mut Connection, _files: ValuesRef<PathBuf>) {
|
||||
self.process_events_count += 1;
|
||||
}
|
||||
|
||||
fn run_webui(&mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool) {
|
||||
self.run_webui_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -378,6 +440,16 @@ mod cli_test {
|
||||
process_command_line(&mut conn, &m, &mut logic);
|
||||
assert_eq!(logic.process_events_count, 1);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn run_web_ui() {
|
||||
// let cmd = command_line_parser();
|
||||
// let m = cmd.get_matches_from(vec!["test", "webui", "run"]);
|
||||
// let mut logic = TestCommandLineLogic::new();
|
||||
// let mut conn = Connection::open_in_memory().unwrap();
|
||||
// process_command_line(&mut conn, &m, &mut logic);
|
||||
// assert_eq!(logic.run_webui_count, 1);
|
||||
// }
|
||||
}
|
||||
|
||||
struct ProCommandLineLogic {}
|
||||
@@ -426,6 +498,11 @@ impl CommandLineUserLogic for ProCommandLineLogic {
|
||||
|
||||
println!("fuck me");
|
||||
}
|
||||
|
||||
fn run_webui(&mut self, conn: &mut Connection, port: u16, enable_template_autoreload: bool) {
|
||||
info!("Serving web-ui on port {:?}", port);
|
||||
web_ui_main(port, enable_template_autoreload);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_connection(m: &ArgMatches) -> Connection {
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::util::*;
|
||||
|
||||
fn _move_securities(tx: &Savepoint,
|
||||
cheque: &Cheque,
|
||||
account_id: &AccountId) -> CustomResult<(accounts::Receipt, accounts::Receipt)> {
|
||||
account_id: &AccountId) -> CustomResult<(accounts::AccountChangeReceipt, accounts::AccountChangeReceipt)> {
|
||||
let drawer_receipt = accounts::add_to_account(tx, &cheque.drawer_account_id, -cheque.amount)?;
|
||||
let payee_receipt = accounts::add_to_account(tx, account_id, cheque.amount)?;
|
||||
if drawer_receipt.amount < 0 {
|
||||
@@ -20,8 +20,8 @@ fn _move_securities(tx: &Savepoint,
|
||||
|
||||
fn _add_cheque_to_cashed_cheques_log(tx: &Connection,
|
||||
cheque: &Cheque,
|
||||
drawer_receipt: &accounts::Receipt,
|
||||
payee_receipt: &accounts::Receipt) -> Result<(), Error> {
|
||||
drawer_receipt: &accounts::AccountChangeReceipt,
|
||||
payee_receipt: &accounts::AccountChangeReceipt) -> Result<(), Error> {
|
||||
let q = "
|
||||
INSERT INTO cashed_cheques(
|
||||
drawer_user_id,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
mod views;
|
||||
|
||||
use actix_utils::future::{ready, Ready};
|
||||
use minijinja;
|
||||
use minijinja_autoreload::AutoReloader;
|
||||
use std::{collections::HashMap, env, fs, io, path::PathBuf};
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::Path;
|
||||
|
||||
use actix_web::{
|
||||
dev::{ServiceResponse},
|
||||
error,
|
||||
http::{header::ContentType, StatusCode},
|
||||
middleware::{ErrorHandlerResponse, ErrorHandlers, Logger},
|
||||
web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, Responder, Result,
|
||||
};
|
||||
use actix_web_lab::respond::Html;
|
||||
|
||||
struct MiniJinjaRenderer {
|
||||
tmpl_env: web::Data<minijinja_autoreload::AutoReloader>,
|
||||
}
|
||||
|
||||
impl MiniJinjaRenderer {
|
||||
fn render(
|
||||
&self,
|
||||
tmpl: &str,
|
||||
ctx: impl Into<minijinja::value::Value>,
|
||||
) -> actix_web::Result<Html> {
|
||||
log::info!("Rendering template {:?}", tmpl);
|
||||
self.tmpl_env
|
||||
.acquire_env()
|
||||
.map_err(|_| error::ErrorInternalServerError("could not acquire template env"))?
|
||||
.get_template(tmpl)
|
||||
.map_err(|e| error::ErrorInternalServerError(format!("{:?}", e)))?
|
||||
.render(ctx.into())
|
||||
.map(Html)
|
||||
.map_err(|err| {
|
||||
log::error!("{err}");
|
||||
error::ErrorInternalServerError("template error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for MiniJinjaRenderer {
|
||||
type Error = actix_web::Error;
|
||||
type Future = actix_utils::future::Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _pl: &mut actix_web::dev::Payload) -> Self::Future {
|
||||
let tmpl_env = <web::Data<minijinja_autoreload::AutoReloader>>::extract(req)
|
||||
.into_inner()
|
||||
.unwrap();
|
||||
|
||||
actix_utils::future::ready(Ok(Self { tmpl_env }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn web_ui_main(port: u16, enable_template_autoreload: bool) -> std::io::Result<()> {
|
||||
// The closure is invoked every time the environment is outdated to recreate it.
|
||||
let tmpl_reloader = AutoReloader::new(move |notifier| {
|
||||
let mut env: minijinja::Environment<'static> = minijinja::Environment::new();
|
||||
|
||||
let tmpl_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
|
||||
log::info!("Using template dir {:?}", tmpl_path);
|
||||
|
||||
// if watch_path is never called, no fs watcher is created
|
||||
if enable_template_autoreload {
|
||||
notifier.watch_path(&tmpl_path, true);
|
||||
}
|
||||
env.set_source(minijinja::Source::from_path(tmpl_path));
|
||||
|
||||
Ok(env)
|
||||
});
|
||||
|
||||
let tmpl_reloader = web::Data::new(tmpl_reloader);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(tmpl_reloader.clone())
|
||||
.service(views::index)
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found))
|
||||
.wrap(Logger::default())
|
||||
})
|
||||
.workers(2)
|
||||
.bind(("0.0.0.0", port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Error handler for a 404 Page not found error.
|
||||
fn not_found<B>(svc_res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
|
||||
let res = get_error_response(&svc_res, "Page not found");
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
|
||||
svc_res.into_parts().0,
|
||||
res.map_into_right_body(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Generic error handler.
|
||||
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse {
|
||||
let req = res.request();
|
||||
|
||||
let tmpl_env = MiniJinjaRenderer::extract(req).into_inner().unwrap();
|
||||
|
||||
// Provide a fallback to a simple plain text response in case an error occurs during the
|
||||
// rendering of the error page.
|
||||
let fallback = |err: &str| {
|
||||
HttpResponse::build(res.status())
|
||||
.content_type(ContentType::plaintext())
|
||||
.body(err.to_string())
|
||||
};
|
||||
|
||||
let ctx = minijinja::context! {
|
||||
error => error,
|
||||
status_code => res.status().as_str(),
|
||||
};
|
||||
|
||||
match tmpl_env.render("error.html", ctx) {
|
||||
Ok(body) => body
|
||||
.customize()
|
||||
.with_status(res.status())
|
||||
.respond_to(req)
|
||||
.map_into_boxed_body(),
|
||||
|
||||
Err(_) => fallback(error),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use actix_web::{Responder, web};
|
||||
use crate::webui::MiniJinjaRenderer;
|
||||
|
||||
#[actix_web::get("/")]
|
||||
pub async fn index(
|
||||
tmpl_env: MiniJinjaRenderer,
|
||||
query: web::Query<HashMap<String, String>>,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
tmpl_env.render("index.html", ())
|
||||
}
|
||||
Reference in new issue
Block a user