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) {
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))

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;
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<T> = std::result::Result<T, Error>;
fn make_id_key(cookie_value: &str) -> async_session::Result<String> {
debug!("deserializing cookie");
Ok(Session::id_from_cookie_value(cookie_value)?)
@ -77,13 +83,20 @@ fn check_pass(payload: &String, pass: &Secret<String>) -> bool {
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]
pub trait AuthStore: SessionStore {
/// 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.
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<B>) -> Result<Self, Self::Rejection> {
let Extension(session_store) = Extension::<SqliteStore>::from_request(req)
async fn from_request(req: &mut RequestParts<B>) -> std::result::Result<Self, Self::Rejection> {
let Extension(session_store) = Extension::<Arc<SqliteStore>>::from_request(req)
.await
.expect("No Session store configured!");
let cookies = Option::<TypedHeader<Cookie>>::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<bool> {
async fn check_user_creds(&self, user_creds: &UserCreds) -> Result<bool> {
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<Option<String>, recipe_store::Error>;
async fn get_recipes_for_user(
&self,
user_id: &str,
) -> Result<Option<Vec<RecipeEntry>>, recipe_store::Error>;
}
#[async_trait]
impl APIStore for SqliteStore {
async fn get_categories_for_user(
&self,
user_id: &str,
) -> Result<Option<String>, recipe_store::Error> {
async fn get_categories_for_user(&self, user_id: &str) -> Result<Option<String>> {
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<Option<Vec<RecipeEntry>>, recipe_store::Error> {
async fn get_recipes_for_user(&self, user_id: &str) -> Result<Option<Vec<RecipeEntry>>> {
// 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<String>,
};
}
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(

View File

@ -107,6 +107,9 @@ where
{
Some(s) => {
let parsed = from_str::<String>(&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) => {