diff --git a/kitchen/src/web/mod.rs b/kitchen/src/web/mod.rs index 91fad27..10222de 100644 --- a/kitchen/src/web/mod.rs +++ b/kitchen/src/web/mod.rs @@ -146,9 +146,11 @@ pub async fn add_user(store_path: PathBuf, username: String, password: String) { pub async fn ui_main(recipe_dir_path: PathBuf, store_path: PathBuf, listen_socket: SocketAddr) { let store = Arc::new(recipe_store::AsyncFileStore::new(recipe_dir_path.clone())); //let dir_path = (&dir_path).clone(); - let app_store = storage::SqliteStore::new(store_path) - .await - .expect("Unable to create app_store"); + let app_store = Arc::new( + storage::SqliteStore::new(store_path) + .await + .expect("Unable to create app_store"), + ); let router = Router::new() .route("/", get(|| async { Redirect::temporary("/ui/plan") })) .route("/ui/*path", get(ui_static_assets)) diff --git a/kitchen/src/web/storage/error.rs b/kitchen/src/web/storage/error.rs new file mode 100644 index 0000000..b2bf71a --- /dev/null +++ b/kitchen/src/web/storage/error.rs @@ -0,0 +1,63 @@ +// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sqlx::Error as SqliteErr; +use tracing::error; + +#[derive(Debug, Clone)] +pub enum Error { + IO(String), + Protocol(String), + BadQuery(String), + Timeout, + NoRecords, + Configuration(String), + MalformedData(String), + InternalError(String), +} + +impl From for Error { + fn from(e: SqliteErr) -> Self { + match e { + SqliteErr::Configuration(e) => Error::Configuration(format!("{:?}", e)), + SqliteErr::PoolTimedOut => Error::Timeout, + SqliteErr::PoolClosed => Error::InternalError(format!("Pool Closed")), + SqliteErr::WorkerCrashed => Error::InternalError(format!("Worker Crashed!")), + SqliteErr::Database(e) => Error::InternalError(format!("{:?}", e)), + SqliteErr::Io(e) => Error::IO(format!("{:?}", e)), + SqliteErr::Tls(e) => Error::Protocol(format!("{:?}", e)), + SqliteErr::Protocol(e) => Error::Protocol(format!("{:?}", e)), + SqliteErr::RowNotFound => Error::NoRecords, + SqliteErr::TypeNotFound { type_name } => { + Error::BadQuery(format!("Type not found `{}`", type_name)) + } + SqliteErr::ColumnIndexOutOfBounds { index, len } => { + Error::BadQuery(format!("column index {} out of bounds for {}", index, len)) + } + SqliteErr::ColumnNotFound(col) => { + Error::BadQuery(format!("Column not found `{}`", col)) + } + SqliteErr::ColumnDecode { index, source } => Error::MalformedData(format!( + "Column index {} can't be decoded: {}", + index, source + )), + SqliteErr::Decode(e) => Error::MalformedData(format!("Decode error: {}", e)), + SqliteErr::Migrate(_) => todo!(), + err => { + error!(?err, "Unhandled Error type encountered"); + Error::InternalError(format!("Unhandled Error type encountered {:?}", err)) + } + } + } +} diff --git a/kitchen/src/web/storage.rs b/kitchen/src/web/storage/mod.rs similarity index 87% rename from kitchen/src/web/storage.rs rename to kitchen/src/web/storage/mod.rs index cfa8f08..bfd940c 100644 --- a/kitchen/src/web/storage.rs +++ b/kitchen/src/web/storage/mod.rs @@ -38,6 +38,10 @@ use tracing::{debug, error, info, instrument}; use recipe_store::RecipeEntry; +mod error; + +pub use error::*; + pub const AXUM_SESSION_COOKIE_NAME: &'static str = "kitchen-session-cookie"; #[derive(Debug, Serialize, Deserialize)] @@ -60,6 +64,8 @@ impl UserCreds { } } +pub type Result = std::result::Result; + fn make_id_key(cookie_value: &str) -> async_session::Result { debug!("deserializing cookie"); Ok(Session::id_from_cookie_value(cookie_value)?) @@ -77,13 +83,20 @@ fn check_pass(payload: &String, pass: &Secret) -> bool { check.is_ok() } +#[async_trait] +pub trait APIStore { + async fn get_categories_for_user(&self, user_id: &str) -> Result>; + + async fn get_recipes_for_user(&self, user_id: &str) -> Result>>; +} + #[async_trait] pub trait AuthStore: SessionStore { /// Check user credentials against the user store. - async fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result; + async fn check_user_creds(&self, user_creds: &UserCreds) -> Result; /// Insert or update user credentials in the user store. - async fn store_user_creds(&self, user_creds: UserCreds) -> async_session::Result<()>; + async fn store_user_creds(&self, user_creds: UserCreds) -> Result<()>; } #[async_trait] @@ -94,13 +107,13 @@ where type Rejection = (StatusCode, &'static str); #[instrument(skip_all)] - async fn from_request(req: &mut RequestParts) -> Result { - let Extension(session_store) = Extension::::from_request(req) + async fn from_request(req: &mut RequestParts) -> std::result::Result { + let Extension(session_store) = Extension::>::from_request(req) .await .expect("No Session store configured!"); let cookies = Option::>::from_request(req) .await - .unwrap(); + .expect("Unable to get headers fromrequest"); // TODO(jwall): We should really validate the expiration and such on this cookie. if let Some(session_cookie) = cookies .as_ref() @@ -203,7 +216,7 @@ impl SessionStore for SqliteStore { #[async_trait] impl AuthStore for SqliteStore { #[instrument(fields(user=%user_creds.id.0, conn_string=self.url), skip_all)] - async fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result { + async fn check_user_creds(&self, user_creds: &UserCreds) -> Result { let id = user_creds.user_id().to_owned(); if let Some(payload) = sqlx::query_scalar!("select password_hashed from users where id = ?", id) @@ -217,7 +230,7 @@ impl AuthStore for SqliteStore { } #[instrument(fields(user=%user_creds.id.0, conn_string=self.url), skip_all)] - async fn store_user_creds(&self, user_creds: UserCreds) -> async_session::Result<()> { + async fn store_user_creds(&self, user_creds: UserCreds) -> Result<()> { let salt = SaltString::generate(&mut OsRng); let password_hash = Argon2::default() .hash_password(user_creds.pass.expose_secret().as_bytes(), &salt) @@ -237,45 +250,22 @@ impl AuthStore for SqliteStore { } // TODO(jwall): We need to do some serious error modeling here. -#[async_trait] -pub trait APIStore { - async fn get_categories_for_user( - &self, - user_id: &str, - ) -> Result, recipe_store::Error>; - - async fn get_recipes_for_user( - &self, - user_id: &str, - ) -> Result>, recipe_store::Error>; -} - #[async_trait] impl APIStore for SqliteStore { - async fn get_categories_for_user( - &self, - user_id: &str, - ) -> Result, recipe_store::Error> { + async fn get_categories_for_user(&self, user_id: &str) -> Result> { match sqlx::query_scalar!( "select category_text from categories where user_id = ?", user_id, ) .fetch_optional(self.pool.as_ref()) - .await + .await? { - Ok(Some(result)) => return Ok(result), - Ok(None) => return Ok(None), - Err(err) => { - error!(?err, "Error getting categories from sqlite db"); - return Err(recipe_store::Error::from(format!("{:?}", err))); - } + Some(result) => Ok(result), + None => Ok(None), } } - async fn get_recipes_for_user( - &self, - user_id: &str, - ) -> Result>, recipe_store::Error> { + async fn get_recipes_for_user(&self, user_id: &str) -> Result>> { // NOTE(jwall): We allow dead code becaue Rust can't figure out that // this code is actually constructed but it's done via the query_as // macro. @@ -283,15 +273,14 @@ impl APIStore for SqliteStore { struct RecipeRow { pub recipe_id: String, pub recipe_text: Option, - }; + } let rows = sqlx::query_as!( RecipeRow, "select recipe_id, recipe_text from recipes where user_id = ?", user_id, ) .fetch_all(self.pool.as_ref()) - .await - .map_err(|e| format!("{:?}", e))? + .await? .iter() .map(|row| { RecipeEntry( diff --git a/web/src/service.rs b/web/src/service.rs index 1351b18..1a2abb6 100644 --- a/web/src/service.rs +++ b/web/src/service.rs @@ -107,6 +107,9 @@ where { Some(s) => { let parsed = from_str::(&s).map_err(|e| format!("{}", e))?; + if parsed.is_empty() { + return Ok(None); + } match parse::as_categories(&parsed) { Ok(categories) => Ok(Some(categories)), Err(e) => {