diff --git a/resources/migrations/sql/create_tables_001.sql b/resources/migrations/sql/create_tables_001.sql index 1069eb3..0be5d6c 100644 --- a/resources/migrations/sql/create_tables_001.sql +++ b/resources/migrations/sql/create_tables_001.sql @@ -14,7 +14,7 @@ VALUES ('aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkk'); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, + name TEXT NOT NULL UNIQUE, public_key TEXT NOT NULL, creation_time DATETIME DEFAULT CURRENT_TIMESTAMP ); diff --git a/resources/static/favicons/android-chrome-192x192.png b/resources/static/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..7a32768 Binary files /dev/null and b/resources/static/favicons/android-chrome-192x192.png differ diff --git a/resources/static/favicons/android-chrome-512x512.png b/resources/static/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..bfde3bc Binary files /dev/null and b/resources/static/favicons/android-chrome-512x512.png differ diff --git a/resources/static/favicons/apple-touch-icon.png b/resources/static/favicons/apple-touch-icon.png new file mode 100644 index 0000000..5f17456 Binary files /dev/null and b/resources/static/favicons/apple-touch-icon.png differ diff --git a/resources/static/favicons/favicon-16x16.png b/resources/static/favicons/favicon-16x16.png new file mode 100644 index 0000000..e46cf3a Binary files /dev/null and b/resources/static/favicons/favicon-16x16.png differ diff --git a/resources/static/favicons/favicon-32x32.png b/resources/static/favicons/favicon-32x32.png new file mode 100644 index 0000000..47545b0 Binary files /dev/null and b/resources/static/favicons/favicon-32x32.png differ diff --git a/resources/static/favicons/favicon.ico b/resources/static/favicons/favicon.ico new file mode 100644 index 0000000..4b5a436 Binary files /dev/null and b/resources/static/favicons/favicon.ico differ diff --git a/resources/static/favicons/site.webmanifest b/resources/static/favicons/site.webmanifest new file mode 100644 index 0000000..bdcf016 --- /dev/null +++ b/resources/static/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/static/favicons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/favicons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/resources/templates/base.html b/resources/templates/base.html index 410163c..f2eaf59 100644 --- a/resources/templates/base.html +++ b/resources/templates/base.html @@ -3,6 +3,12 @@ + + + + + + ledger diff --git a/resources/templates/partials/index/failure.html b/resources/templates/partials/index/failure.html new file mode 100644 index 0000000..58058c3 --- /dev/null +++ b/resources/templates/partials/index/failure.html @@ -0,0 +1,16 @@ +
+

+ Sorry mate, that was plainly wrong. +

+
+ + +
+ +
+ +
+
diff --git a/resources/templates/partials/index/form.html b/resources/templates/partials/index/form.html index 31c2e32..d064ce9 100644 --- a/resources/templates/partials/index/form.html +++ b/resources/templates/partials/index/form.html @@ -4,8 +4,8 @@ hx-target="#home-page-main-block">
diff --git a/resources/templates/partials/index/success.html b/resources/templates/partials/index/success.html index 6ec4fa7..c8d9768 100644 --- a/resources/templates/partials/index/success.html +++ b/resources/templates/partials/index/success.html @@ -1,7 +1,7 @@

- You are logged in as {{ name }}. + Congrat! You are now logged in as {{ user.name }}.

diff --git a/src/db/_users.rs b/src/db/_users.rs index 1131f91..74af1bc 100644 --- a/src/db/_users.rs +++ b/src/db/_users.rs @@ -1,4 +1,4 @@ -use rusqlite::{params, Connection, Result, Row, Transaction}; +use rusqlite::{params, Connection, Result, Row, Transaction, OptionalExtension}; use serde::Serialize; use crate::db::_external_id::{Id, UserId}; @@ -22,12 +22,12 @@ impl User { } pub fn create_user(tx: &Transaction, name: &String, public_key: &String) -> CustomResult { - tx.execute( - "INSERT INTO users (name, public_key) VALUES (?1, ?2)", + tx.query_row( + "INSERT INTO users (name, public_key) VALUES (?1, ?2) RETURNING *", (name, public_key), + _user_from_row ) - .map_err(Error::SqlError)?; - return get_user_by_id(tx, tx.last_insert_rowid() as i32); + .map_err(Error::SqlError) } fn _user_from_row(row: &Row) -> Result { @@ -38,11 +38,21 @@ fn _user_from_row(row: &Row) -> Result { }) } -fn get_user_by_id(tx: &Transaction, id: i32) -> CustomResult { +fn get_user_by_id(tx: &Transaction, id: & UserId) -> CustomResult> { let q = "SELECT id, name, public_key FROM users WHERE id = ?1"; tx.query_row(q, params![id], _user_from_row) + .optional() + .map_err(Error::SqlError) +} + +pub fn get_user_by_name(tx: &Connection, name: &String) -> CustomResult>{ + let q = "SELECT id, name, public_key + FROM users + WHERE name = ?1"; + tx.query_row(q, params![name], _user_from_row) + .optional() .map_err(Error::SqlError) } @@ -58,3 +68,61 @@ pub fn list_users(conn: &Connection) -> CustomResult> { .collect(); return Ok(ret); } + +#[cfg(test)] +mod test_users { + use crate::migration; + use super::*; + + fn setup() -> Connection{ + let mut conn = Connection::open_in_memory().unwrap(); + migration::apply_migrations(&mut conn).unwrap(); + conn + } + + fn setup_with_one_user() -> (Connection, User){ + let mut conn = setup(); + let tx = conn.transaction().unwrap(); + let u = create_user(& tx, & "bob".to_string(), & "123".to_string()).unwrap(); + tx.commit().unwrap(); + (conn, u) + } + + #[test] + fn test_happy_path() -> CustomResult<()>{ + let mut conn = setup(); + let tx = conn.transaction().unwrap(); + let u = create_user(& tx, & "alice".to_string(), & "123".to_string())?; + tx.commit().unwrap(); + + let tx = conn.transaction().unwrap(); + assert_eq!( list_users(& tx)?.len(), 1); + + assert!( get_user_by_name(&tx, & u.name)?.is_some()); + Ok(()) + } + + #[test] + fn test_get_user() -> CustomResult<()>{ + let (mut conn, user) = setup_with_one_user(); + let tx = conn.transaction().unwrap(); + + assert!( get_user_by_name(&tx, & user.name)?.is_some()); + assert!( get_user_by_name(&tx, & "404-not-found".to_string())?.is_none()); + Ok(()) + } + + #[test] + fn test_get_user_by_id() -> CustomResult<()> { + let (mut conn, user) = setup_with_one_user(); + let tx = conn.transaction().unwrap(); + + assert!( get_user_by_id(&tx, & user.id())?.is_some()); + + let random_id = UserId::from_id(8734289723902734); + + assert!( get_user_by_id(&tx, & random_id)?.is_none()); + Ok(()) + } + +} diff --git a/src/db/mod.rs b/src/db/mod.rs index c46f675..63f6a21 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -22,7 +22,7 @@ pub mod securities { } pub mod users { - pub use super::_users::{create_user, list_users, User}; + pub use super::_users::{create_user, list_users, get_user_by_name, User}; } pub mod accounts { diff --git a/src/services/_authentication.rs b/src/services/_authentication.rs index c5d94ab..e6993fb 100644 --- a/src/services/_authentication.rs +++ b/src/services/_authentication.rs @@ -11,6 +11,13 @@ pub struct SessionService { } impl SessionService { + + pub fn get_key(conn: &mut Connection) -> CustomResult { + let tx = conn.transaction().map_err(Error::SqlError)?; + let secret = LedgerInternal::new().get_cookie_secret(&tx)?; + Ok(Key::derive_from(&secret)) + } + pub fn from_conn(conn: &mut Connection) -> CustomResult { let tx = conn.transaction().map_err(Error::SqlError)?; let secret = LedgerInternal::new().get_cookie_secret(&tx)?; @@ -20,6 +27,10 @@ impl SessionService { Ok(ret) } + pub fn from_key(key: Key) -> SessionService{ + SessionService{key} + } + pub fn create_login_cookie(&self, user_id: &String) -> Cookie { let mut jar = CookieJar::new(); jar.private_mut(&self.key) diff --git a/src/services/_users.rs b/src/services/_users.rs index a2acab6..ea81b43 100644 --- a/src/services/_users.rs +++ b/src/services/_users.rs @@ -2,7 +2,7 @@ use rusqlite::Connection; use serde::Deserialize; use serde::Serialize; -use crate::db::users::{list_users, User}; +use crate::db::users::{get_user_by_name, list_users, User}; use crate::util::CustomResult; #[derive(Serialize, Debug)] @@ -28,10 +28,10 @@ where } } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct UserDTO { - name: String, - id: String, // todo: should be of type UserId + pub name: String, + pub id: String, // todo: should be of type UserId } impl UserDTO { @@ -52,6 +52,11 @@ impl UserService<'_> { UserService { conn } } + pub fn get_user_for_authentication(&self, username: &String, _credentials: &String) -> CustomResult> { + let ret = get_user_by_name(self.conn, username)?; + Ok(ret.as_ref().map(UserDTO::from_entity)) + } + pub fn list_users(self) -> CustomResult> { let users = list_users(self.conn)? .iter() diff --git a/src/webui/index_views.rs b/src/webui/index_views.rs index b8bb2b7..65c51f5 100644 --- a/src/webui/index_views.rs +++ b/src/webui/index_views.rs @@ -11,16 +11,27 @@ pub struct LoginPayload { password: String, } +fn logout_req(){ + +} + #[actix_web::post("/login")] pub async fn login( tmpl_env: MiniJinjaRenderer, web::Json(form): web::Json, service_factory: web::Data, ) -> actix_web::Result { + let conn = service_factory.get_connection()?; + let user_service = service_factory.get_user_service(&conn)?; + let maybeUser = user_service.get_user_for_authentication(&form.username, &form.password).unwrap(); + if maybeUser.is_none() { + return Ok(tmpl_env.render("partials/index/failure.html", context!{})?.customize()); + } + let user = maybeUser.unwrap(); let authentication_service = service_factory.get_authentication_service()?; - let cookie = authentication_service.create_login_cookie(&form.username); + let cookie = authentication_service.create_login_cookie(&user.id); let ctx = context! { - name => form.username, + user => & user, }; let ret = tmpl_env .render("partials/index/success.html", ctx)? @@ -46,12 +57,16 @@ pub async fn index( service_factory: web::Data, tmpl_env: MiniJinjaRenderer, ) -> actix_web::Result { + let conn = service_factory.get_connection()?; let authentication_service = service_factory.get_authentication_service()?; + let user_service = service_factory.get_user_service(&conn)?; + let userid = authentication_service.get_login(&req); + let username = userid.clone().unwrap_or("".to_string()); let ctx = context! { is_logged_in => userid.is_some(), - name => username, + // user => user_service.get_user_for_authentication(userid.unwrap(), }; tmpl_env.render("index.html", ctx) } diff --git a/src/webui/services.rs b/src/webui/services.rs index c3d04b4..c41c111 100644 --- a/src/webui/services.rs +++ b/src/webui/services.rs @@ -5,6 +5,7 @@ use r2d2::PooledConnection; use r2d2_sqlite::SqliteConnectionManager; use std::path::PathBuf; +use cookie::Key; pub type DbPool = r2d2::Pool; @@ -15,7 +16,7 @@ pub fn creat_db_pool(path: &PathBuf) -> DbPool { #[derive(Clone)] struct ServicesCache { - session_service: SessionService, + session_service_key: Key, } #[derive(Clone)] @@ -27,8 +28,8 @@ pub struct ServicesFactory { 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 }; + let session_service_key = SessionService::get_key(&mut conn).unwrap(); + let cache = ServicesCache { session_service_key }; ServicesFactory { pool, cache } } @@ -39,7 +40,7 @@ impl ServicesFactory { } pub fn get_authentication_service(&self) -> Result { - Ok(self.cache.session_service.clone()) + Ok(SessionService::from_key(self.cache.session_service_key.clone())) } pub fn get_user_service<'a>(