mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Error modeling for our Storage layer
This commit is contained in:
parent
f87196e4c8
commit
5750b33673
@ -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(
|
||||||
|
storage::SqliteStore::new(store_path)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to create app_store");
|
.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))
|
||||||
|
63
kitchen/src/web/storage/error.rs
Normal file
63
kitchen/src/web/storage/error.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
@ -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) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user