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) {
|
||||
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)
|
||||
let app_store = Arc::new(
|
||||
storage::SqliteStore::new(store_path)
|
||||
.await
|
||||
.expect("Unable to create app_store");
|
||||
.expect("Unable to create app_store"),
|
||||
);
|
||||
let router = Router::new()
|
||||
.route("/", get(|| async { Redirect::temporary("/ui/plan") }))
|
||||
.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;
|
||||
|
||||
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(
|
@ -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) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user