Error modeling for our Storage layer

This commit is contained in:
Jeremy Wall 2022-09-03 12:20:59 -04:00
parent f87196e4c8
commit 5750b33673
4 changed files with 98 additions and 41 deletions

View File

@ -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) { 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 store = Arc::new(recipe_store::AsyncFileStore::new(recipe_dir_path.clone()));
//let dir_path = (&dir_path).clone(); //let dir_path = (&dir_path).clone();
let app_store = storage::SqliteStore::new(store_path) let app_store = Arc::new(
.await storage::SqliteStore::new(store_path)
.expect("Unable to create app_store"); .await
.expect("Unable to create app_store"),
);
let router = Router::new() let router = Router::new()
.route("/", get(|| async { Redirect::temporary("/ui/plan") })) .route("/", get(|| async { Redirect::temporary("/ui/plan") }))
.route("/ui/*path", get(ui_static_assets)) .route("/ui/*path", get(ui_static_assets))

View File

@ -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<SqliteErr> 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))
}
}
}
}

View File

@ -38,6 +38,10 @@ use tracing::{debug, error, info, instrument};
use recipe_store::RecipeEntry; use recipe_store::RecipeEntry;
mod error;
pub use error::*;
pub const AXUM_SESSION_COOKIE_NAME: &'static str = "kitchen-session-cookie"; pub const AXUM_SESSION_COOKIE_NAME: &'static str = "kitchen-session-cookie";
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -60,6 +64,8 @@ impl UserCreds {
} }
} }
pub type Result<T> = std::result::Result<T, Error>;
fn make_id_key(cookie_value: &str) -> async_session::Result<String> { fn make_id_key(cookie_value: &str) -> async_session::Result<String> {
debug!("deserializing cookie"); debug!("deserializing cookie");
Ok(Session::id_from_cookie_value(cookie_value)?) Ok(Session::id_from_cookie_value(cookie_value)?)
@ -77,13 +83,20 @@ fn check_pass(payload: &String, pass: &Secret<String>) -> bool {
check.is_ok() check.is_ok()
} }
#[async_trait]
pub trait APIStore {
async fn get_categories_for_user(&self, user_id: &str) -> Result<Option<String>>;
async fn get_recipes_for_user(&self, user_id: &str) -> Result<Option<Vec<RecipeEntry>>>;
}
#[async_trait] #[async_trait]
pub trait AuthStore: SessionStore { pub trait AuthStore: SessionStore {
/// Check user credentials against the user store. /// Check user credentials against the user store.
async fn check_user_creds(&self, user_creds: &UserCreds) -> async_session::Result<bool>; async fn check_user_creds(&self, user_creds: &UserCreds) -> Result<bool>;
/// Insert or update user credentials in the user store. /// 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] #[async_trait]
@ -94,13 +107,13 @@ where
type Rejection = (StatusCode, &'static str); type Rejection = (StatusCode, &'static str);
#[instrument(skip_all)] #[instrument(skip_all)]
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> { async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let Extension(session_store) = Extension::<SqliteStore>::from_request(req) let Extension(session_store) = Extension::<Arc<SqliteStore>>::from_request(req)
.await .await
.expect("No Session store configured!"); .expect("No Session store configured!");
let cookies = Option::<TypedHeader<Cookie>>::from_request(req) let cookies = Option::<TypedHeader<Cookie>>::from_request(req)
.await .await
.unwrap(); .expect("Unable to get headers fromrequest");
// TODO(jwall): We should really validate the expiration and such on this cookie. // TODO(jwall): We should really validate the expiration and such on this cookie.
if let Some(session_cookie) = cookies if let Some(session_cookie) = cookies
.as_ref() .as_ref()
@ -203,7 +216,7 @@ impl SessionStore for SqliteStore {
#[async_trait] #[async_trait]
impl AuthStore for SqliteStore { impl AuthStore for SqliteStore {
#[instrument(fields(user=%user_creds.id.0, conn_string=self.url), skip_all)] #[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<bool> { async fn check_user_creds(&self, user_creds: &UserCreds) -> Result<bool> {
let id = user_creds.user_id().to_owned(); let id = user_creds.user_id().to_owned();
if let Some(payload) = if let Some(payload) =
sqlx::query_scalar!("select password_hashed from users where id = ?", id) 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)] #[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 salt = SaltString::generate(&mut OsRng);
let password_hash = Argon2::default() let password_hash = Argon2::default()
.hash_password(user_creds.pass.expose_secret().as_bytes(), &salt) .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. // 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<Option<String>, recipe_store::Error>;
async fn get_recipes_for_user(
&self,
user_id: &str,
) -> Result<Option<Vec<RecipeEntry>>, recipe_store::Error>;
}
#[async_trait] #[async_trait]
impl APIStore for SqliteStore { impl APIStore for SqliteStore {
async fn get_categories_for_user( async fn get_categories_for_user(&self, user_id: &str) -> Result<Option<String>> {
&self,
user_id: &str,
) -> Result<Option<String>, recipe_store::Error> {
match sqlx::query_scalar!( match sqlx::query_scalar!(
"select category_text from categories where user_id = ?", "select category_text from categories where user_id = ?",
user_id, user_id,
) )
.fetch_optional(self.pool.as_ref()) .fetch_optional(self.pool.as_ref())
.await .await?
{ {
Ok(Some(result)) => return Ok(result), Some(result) => Ok(result),
Ok(None) => return Ok(None), None => Ok(None),
Err(err) => {
error!(?err, "Error getting categories from sqlite db");
return Err(recipe_store::Error::from(format!("{:?}", err)));
}
} }
} }
async fn get_recipes_for_user( async fn get_recipes_for_user(&self, user_id: &str) -> Result<Option<Vec<RecipeEntry>>> {
&self,
user_id: &str,
) -> Result<Option<Vec<RecipeEntry>>, recipe_store::Error> {
// NOTE(jwall): We allow dead code becaue Rust can't figure out that // 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 // this code is actually constructed but it's done via the query_as
// macro. // macro.
@ -283,15 +273,14 @@ impl APIStore for SqliteStore {
struct RecipeRow { struct RecipeRow {
pub recipe_id: String, pub recipe_id: String,
pub recipe_text: Option<String>, pub recipe_text: Option<String>,
}; }
let rows = sqlx::query_as!( let rows = sqlx::query_as!(
RecipeRow, RecipeRow,
"select recipe_id, recipe_text from recipes where user_id = ?", "select recipe_id, recipe_text from recipes where user_id = ?",
user_id, user_id,
) )
.fetch_all(self.pool.as_ref()) .fetch_all(self.pool.as_ref())
.await .await?
.map_err(|e| format!("{:?}", e))?
.iter() .iter()
.map(|row| { .map(|row| {
RecipeEntry( RecipeEntry(

View File

@ -107,6 +107,9 @@ where
{ {
Some(s) => { Some(s) => {
let parsed = from_str::<String>(&s).map_err(|e| format!("{}", e))?; let parsed = from_str::<String>(&s).map_err(|e| format!("{}", e))?;
if parsed.is_empty() {
return Ok(None);
}
match parse::as_categories(&parsed) { match parse::as_categories(&parsed) {
Ok(categories) => Ok(Some(categories)), Ok(categories) => Ok(Some(categories)),
Err(e) => { Err(e) => {