Turn static data within the database into an enum

This commit is contained in:
Z. Charles Dziura 2025-03-03 17:32:40 -05:00
parent 066b912e42
commit 3a93b7e12f
22 changed files with 546 additions and 104 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "debt-pirate" name = "debt-pirate"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
argon2 = "0.5" argon2 = "0.5"
@ -11,8 +11,8 @@ axum = { version = "0.8", features = [
"ws", "ws",
] } ] }
base64 = "0.22" base64 = "0.22"
bb8-redis = "0.20"
blake3 = { version = "1.5", features = ["serde"] } blake3 = { version = "1.5", features = ["serde"] }
deadpool-redis = { version = "0.20", features = ["serde"] }
futures = "0.3" futures = "0.3"
http = "1.0" http = "1.0"
humantime = "2.1" humantime = "2.1"
@ -28,7 +28,7 @@ lettre = { version = "0.11", default-features = false, features = [
] } ] }
num_cpus = "1.16" num_cpus = "1.16"
pasetors = "0.7" pasetors = "0.7"
redis = { version = "0.28", features = ["aio"] } rust_decimal = "1.36"
serde = { version = "1.0", features = ["derive", "rc", "std"] } serde = { version = "1.0", features = ["derive", "rc", "std"] }
serde_json = "1.0" serde_json = "1.0"
serde_with = "3.9" serde_with = "3.9"
@ -37,6 +37,7 @@ sqlx = { version = "0.8", features = [
"time", "time",
"postgres", "postgres",
"runtime-tokio", "runtime-tokio",
"rust_decimal",
] } ] }
syslog-tracing = "0.3.1" syslog-tracing = "0.3.1"
time = { version = "0.3", features = ["formatting", "macros"] } time = { version = "0.3", features = ["formatting", "macros"] }

View file

@ -1,12 +1,17 @@
DROP INDEX IF EXISTS status_name_uniq_idx;
DROP INDEX IF EXISTS user_username_uniq_idx; DROP INDEX IF EXISTS user_username_uniq_idx;
DROP INDEX IF EXISTS user_email_uniq_idx; DROP INDEX IF EXISTS user_email_uniq_idx;
DROP INDEX IF EXISTS permission_name_uniq_idx; DROP INDEX IF EXISTS permission_category_idx;
DROP INDEX IF EXISTS account_type_name_uniq_idx; DROP INDEX IF EXISTS permission_category_name_idx;
DROP TABLE IF EXISTS public.user_account_permission;
DROP TABLE IF EXISTS public.account; DROP TABLE IF EXISTS public.account;
DROP TABLE IF EXISTS public.permission; DROP TABLE IF EXISTS public.permission;
DROP TABLE IF EXISTS public.user; DROP TABLE IF EXISTS public.user;
DROP TABLE IF EXISTS public.transaction_line_item;
DROP TABLE IF EXISTS public.transaction CASCADE;
DROP TABLE IF EXISTS public.budget CASCADE; DROP TABLE IF EXISTS public.budget CASCADE;
DROP TABLE IF EXISTS public.status CASCADE;
DROP TABLE IF EXISTS public.account_type CASCADE; DROP TYPE public.entry_type;
DROP TYPE public.account_type;
DROP TYPE public.permission_category;
DROP TYPE public.status;

View file

@ -1,51 +1,29 @@
CREATE TABLE IF NOT EXISTS CREATE TYPE
public.status ( public.status
AS ENUM
('Active', 'Unverified', 'Removed', 'Quarantined');
CREATE TYPE
public.account_type
AS ENUM
('Asset', 'Equity', 'Expense', 'Liability', 'Revenue');
CREATE TYPE
public.entry_type
AS ENUM
('Debit', 'Credit');
CREATE TYPE
public.permission_category
AS ENUM
('Account');
CREATE TABLE IF NOT EXISTS public.user (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS status_name_uniq_idx ON public.status(name);
INSERT INTO
public.status (
name
)
VALUES
('Active'),
('Unverified'),
('Removed'),
('Quarantined');
CREATE TABLE IF NOT EXISTS
public.account_type (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS account_type_name_uniq_idx ON public.account_type(name);
INSERT INTO
public.account_type (
name
)
VALUES
('Asset'),
('Equity'),
('Expense'),
('Liability'),
('Revenue');
CREATE TABLE IF NOT EXISTS
public.user (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL, email TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
status_id INT NOT NULL REFERENCES status(id) DEFAULT 2, status status NOT NULL DEFAULT 'Unverified',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL updated_at TIMESTAMP WITH TIME ZONE NULL
); );
@ -55,33 +33,81 @@ CREATE UNIQUE INDEX IF NOT EXISTS user_email_uniq_idx ON public.user(email);
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.permission ( public.permission (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category permission_category NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1, value INT NOT NULL,
status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL updated_at TIMESTAMP WITH TIME ZONE NULL
); );
CREATE UNIQUE INDEX IF NOT EXISTS permission_name_uniq_idx ON public.permission(name); CREATE INDEX IF NOT EXISTS permission_category_idx ON public.permission(category);
CREATE INDEX IF NOT EXISTS permission_category_name_idx ON public.permission(category, name);
INSERT INTO
public.permission (
category,
name,
value
)
VALUES
('Account', 'View', 1),
('Account', 'Edit', 2),
('Account', 'Delete', 4),
('Account', 'Grant', 8);
CREATE TABLE IF NOT EXISTS
public.account (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_type account_type NOT NULL,
name TEXT NOT NULL,
description TEXT NULL,
currency_code TEXT NOT NULL,
status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL
);
CREATE TABLE IF NOT EXISTS
public.user_account_permission (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT NOT NULL REFERENCES public.user(id),
account_id INT NOT NULL REFERENCES public.account(id),
permission_id INT NOT NULL REFERENCES public.permission(id),
status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL
);
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.budget ( public.budget (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NULL, description TEXT NULL,
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1, icon TEXT NULL,
status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL updated_at TIMESTAMP WITH TIME ZONE NULL
); );
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS
public.account ( public.transaction (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_type_id INT NOT NULL REFERENCES account_type(id), description TEXT NOT NULL,
budget_id INT NOT NULL REFERENCES budget(id), budget_id INT NOT NULL REFERENCES public.budget(id),
name TEXT NOT NULL, status status NOT NULL DEFAULT 'Active',
description TEXT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
currency_code TEXT NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NULL
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1, );
CREATE TABLE IF NOT EXISTS
public.transaction_line_item (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
transaction_id INT NOT NULL REFERENCES public.transaction(id),
account_id INT NOT NULL REFERENCES public.account(id),
entry_type entry_type NOT NULL,
value NUMERIC(12, 2) NOT NULL,
status status NOT NULL DEFAULT 'Active',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NULL updated_at TIMESTAMP WITH TIME ZONE NULL
); );

55
api/src/db/account.rs Normal file
View file

@ -0,0 +1,55 @@
use sqlx::prelude::*;
use tracing::error;
use crate::models::AppError;
use super::{
associate_account_with_user_as_owner, AccountType, DbPool, PermissionEntity, StatusType,
};
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct NewAccountEntity {
pub name: String,
pub description: Option<String>,
pub account_type: AccountType,
pub currency_code: String,
}
#[allow(dead_code)]
#[derive(Debug, FromRow)]
pub struct AccountEntity {
pub id: i32,
pub account_type: AccountType,
pub name: String,
pub description: Option<String>,
pub currency_code: String,
pub status: StatusType,
}
pub async fn insert_new_account_for_user(
pool: &DbPool,
new_account: NewAccountEntity,
user_id: i32,
) -> Result<(AccountEntity, Vec<PermissionEntity>), AppError> {
let NewAccountEntity {
name,
description,
account_type,
currency_code,
} = &new_account;
let new_account = sqlx::query_as::<_, AccountEntity>("INSERT INTO public.account (name, description, account_type, currency_code) VALUES ($1, $2, $3, $4) RETURNING id, account_type, name, description, currency_code, status;")
.bind(name)
.bind(description)
.bind(account_type)
.bind(currency_code)
.fetch_one(pool).await
.inspect_err(|err| error!(?err, record = ?new_account, "Cannot insert new account record"))
.map_err(AppError::from)?;
let account_permissions =
associate_account_with_user_as_owner(pool, new_account.id, user_id).await?;
Ok((new_account, account_permissions))
}

View file

@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type,
)]
#[sqlx(type_name = "account_type", rename_all = "PascalCase")]
pub enum AccountType {
Asset,
Equity,
Expense,
Liability,
Revenue,
}

View file

@ -1,7 +1,18 @@
mod account;
mod account_type;
mod permission;
mod status;
mod user; mod user;
mod user_account_permission;
pub use account::*;
pub use account_type::*;
pub use permission::*;
pub use status::*;
pub use user::*;
pub use user_account_permission::*;
use sqlx::{self, postgres::PgPoolOptions, Pool, Postgres}; use sqlx::{self, postgres::PgPoolOptions, Pool, Postgres};
pub use user::*;
use crate::models::AppError; use crate::models::AppError;

91
api/src/db/permission.rs Normal file
View file

@ -0,0 +1,91 @@
use serde::{Deserialize, Serialize};
use sqlx::prelude::*;
use tracing::error;
use crate::models::AppError;
use super::DbPool;
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type,
)]
#[sqlx(type_name = "permission_category", rename_all = "PascalCase")]
pub enum PermissionCategoryType {
Account,
}
#[allow(dead_code)]
#[derive(Debug, FromRow)]
pub struct PermissionEntity {
pub id: i32,
pub category: PermissionCategoryType,
pub name: String,
pub value: i32,
}
pub async fn get_all_permissions_by_category(
pool: &DbPool,
category: PermissionCategoryType,
) -> Result<Vec<PermissionEntity>, AppError> {
sqlx::query_as::<_, PermissionEntity>(
"SELECT id, category, name value FROM public.permission WHERE category = $1;",
)
.bind(category)
.fetch_all(pool)
.await
.inspect_err(|err| {
error!(
?err,
?category,
"Unable to fetch permissions associated with category"
)
})
.map_err(From::from)
}
pub async fn _get_permission_by_id(pool: &DbPool, id: i32) -> Result<PermissionEntity, AppError> {
sqlx::query_as::<_, PermissionEntity>(
"SELECT id, category, name, value FROM public.permission WHERE id = $1;",
)
.bind(id)
.fetch_one(pool)
.await
.inspect_err(|err| error!(?err, ?id, "Unable to fetch permission"))
.map_err(From::from)
}
pub async fn get_many_permissions_by_id(
pool: &DbPool,
ids: &[i32],
) -> Result<Vec<PermissionEntity>, AppError> {
let ids = ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
sqlx::query_as::<_, PermissionEntity>(
"SELECT id, category, name, value FROM public.permission WHERE id IN ($1);",
)
.bind(ids.as_str())
.fetch_all(pool)
.await
.inspect_err(|err| error!(?err, ?ids, "Unable to fetch permissions"))
.map_err(From::from)
}
pub async fn _get_permission_by_category_and_name(
pool: &DbPool,
category: &str,
name: &str,
) -> Result<PermissionEntity, AppError> {
sqlx::query_as::<_, PermissionEntity>(
"SELECT id, category, name, value FROM public.permission WHERE category = $1 AND name = $2;"
)
.bind(category)
.bind(name)
.fetch_one(pool)
.await
.inspect_err(|err| error!(?err, ?category, ?name, "Unable to fetch permission"))
.map_err(From::from)
}

12
api/src/db/status.rs Normal file
View file

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type,
)]
#[sqlx(type_name = "status", rename_all = "PascalCase")]
pub enum StatusType {
Active,
Unverified,
Removed,
Quarantined,
}

View file

@ -30,7 +30,6 @@ pub struct UserEntity {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub email: String, pub email: String,
pub status_id: i32,
} }
pub async fn insert_new_user( pub async fn insert_new_user(
@ -43,16 +42,13 @@ pub async fn insert_new_user(
name, name,
} = new_user.clone(); } = new_user.clone();
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (email, password, name) VALUES ($1, $2, $3) RETURNING id, email, name, status_id;") sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (email, password, name) VALUES ($1, $2, $3) RETURNING id, email, name;")
.bind(email) .bind(email)
.bind(password) .bind(password)
.bind(name) .bind(name)
.fetch_one(pool).await .fetch_one(pool).await
.map_err(|err| { .inspect_err(|err| error!(?err, record = ?new_user, "Cannot insert new user record"))
error!(?err, record = ?new_user, "Cannot insert new user record"); .map_err(From::from)
AppError::from(err)
})
} }
#[derive(Debug, FromRow)] #[derive(Debug, FromRow)]
@ -71,20 +67,16 @@ pub async fn get_username_and_password_by_email(
.bind(email) .bind(email)
.fetch_one(pool) .fetch_one(pool)
.await .await
.map_err(|err| { .inspect_err(|err| error!(?err, "Unable to find user"))
error!(?err, "Unable to find user"); .map_err(From::from)
AppError::from(err)
})
} }
pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> { pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;") sqlx::query("UPDATE public.user SET status = 'Active', updated_at = now() WHERE id = $1;")
.bind(user_id) .bind(user_id)
.execute(pool) .execute(pool)
.await .await
.map_err(|err| { .inspect_err(|err| error!(?err, user_id, "Error verifying user"))
error!(?err, user_id, "Error verifying user"); .map_err(From::from)
AppError::from(err)
})
.map(|_| ()) .map(|_| ())
} }

View file

@ -0,0 +1,61 @@
use sqlx::prelude::*;
use tracing::error;
use crate::models::AppError;
use super::{
get_all_permissions_by_category, get_many_permissions_by_id, DbPool, PermissionCategoryType,
PermissionEntity, StatusType,
};
#[allow(dead_code)]
#[derive(Clone)]
pub struct NewUserAccountPermissionEntity {
pub user_id: i32,
pub account_id: i32,
pub permission_id: i32,
}
#[allow(dead_code)]
#[derive(Debug, FromRow)]
pub struct UserAccountPermissionEntity {
pub id: i32,
pub user_id: i32,
pub account_id: i32,
pub permission_id: i32,
pub status: StatusType,
}
pub async fn associate_account_with_user_as_owner(
pool: &DbPool,
account_id: i32,
user_id: i32,
) -> Result<Vec<PermissionEntity>, AppError> {
let values = get_all_permissions_by_category(pool, PermissionCategoryType::Account)
.await?
.into_iter()
.map(|permission| format!("({user_id}, {account_id}, {})", permission.id))
.collect::<Vec<_>>()
.join(",");
let query = format!("INSERT INTO public.user_account_relation (user_id, account_id, permission_id) VALUES {values} RETURNING (id, user_id, account_id, permission_id, status);");
let permission_ids = sqlx::query_as::<_, UserAccountPermissionEntity>(query.as_str())
.fetch_all(pool)
.await
.inspect_err(|err| {
error!(
?err,
account_id, user_id, "Cannot associate account with user as owner"
)
})
.map_err(|err| AppError::from(err))
.map(|relations| {
relations
.into_iter()
.map(|relation| relation.permission_id)
.collect::<Vec<_>>()
})?;
get_many_permissions_by_id(pool, permission_ids.as_slice()).await
}

View file

@ -8,7 +8,7 @@ mod services;
use db::{create_db_connection_pool, run_migrations}; use db::{create_db_connection_pool, run_migrations};
use requests::start_app; use requests::start_app;
use services::{ use services::{
create_cache_connection_pool, initialize_logger, start_emailer_service, UserConfirmationMessage, UserConfirmationMessage, create_cache_connection_pool, initialize_logger, start_emailer_service,
}; };
use tokio::runtime::Handle; use tokio::runtime::Handle;
use tracing::{error, info}; use tracing::{error, info};
@ -38,7 +38,6 @@ async fn main() {
info!("Initializing cache service connection pool..."); info!("Initializing cache service connection pool...");
let cache_pool = create_cache_connection_pool(config.cache_connection_uri().to_string()) let cache_pool = create_cache_connection_pool(config.cache_connection_uri().to_string())
.await
.inspect_err(|err| error!(?err)) .inspect_err(|err| error!(?err))
.unwrap(); .unwrap();
info!("Cache service connection pool created successfully."); info!("Cache service connection pool created successfully.");

View file

@ -6,10 +6,9 @@ use std::{
}; };
use axum::response::IntoResponse; use axum::response::IntoResponse;
use bb8_redis::bb8::RunError; use deadpool_redis::{PoolError, redis::RedisError};
use http::StatusCode; use http::StatusCode;
use redis::RedisError; use sqlx::{Error as SqlxError, error::DatabaseError, migrate::MigrateError};
use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
use toml::{self, de::Error as TomlError}; use toml::{self, de::Error as TomlError};
use tracing::trace; use tracing::trace;
@ -105,8 +104,8 @@ impl From<RedisError> for AppError {
} }
} }
impl From<RunError<RedisError>> for AppError { impl From<PoolError> for AppError {
fn from(other: RunError<RedisError>) -> Self { fn from(other: PoolError) -> Self {
trace!(err = ?other, "Cache pool error"); trace!(err = ?other, "Cache pool error");
Self::new(ErrorKind::Cache(other.to_string())) Self::new(ErrorKind::Cache(other.to_string()))
} }

View file

@ -1,9 +1,9 @@
use std::time::SystemTime; use std::time::SystemTime;
use axum::extract::FromRequestParts; use axum::extract::FromRequestParts;
use deadpool_redis::redis::ToRedisArgs;
use http::request::Parts; use http::request::Parts;
use humantime::format_rfc3339; use humantime::format_rfc3339;
use redis::ToRedisArgs;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{

View file

@ -0,0 +1,53 @@
use axum::{
debug_handler,
extract::State,
response::{IntoResponse, Response},
Json,
};
use http::StatusCode;
use crate::{
db::{insert_new_account_for_user, DbPool, NewAccountEntity},
models::{AppError, Session},
requests::AppState,
};
use super::models::{AccountCreationRequest, AccountCreationResponse};
#[debug_handler]
pub async fn account_creation_post_handler(
State(state): State<AppState>,
session: Session,
Json(request): Json<AccountCreationRequest>,
) -> Result<Response, AppError> {
let pool = state.db_pool();
let user_id = session.user_id;
account_creation_request(request, user_id, pool).await
}
async fn account_creation_request(
request: AccountCreationRequest,
user_id: i32,
pool: &DbPool,
) -> Result<Response, AppError> {
let AccountCreationRequest {
r#type: account_type,
name,
description,
currency_code,
} = request;
let new_account = NewAccountEntity {
name,
description,
account_type: account_type.into(),
currency_code,
};
let response = insert_new_account_for_user(pool, new_account, user_id)
.await
.map(|response| AccountCreationResponse::from(response))?;
Ok((StatusCode::CREATED, Json(response)).into_response())
}

View file

@ -0,0 +1,4 @@
mod handler;
mod models;
pub use handler::*;

View file

@ -0,0 +1,42 @@
mod request;
mod response;
pub use request::*;
pub use response::*;
use serde::{Deserialize, Serialize};
use crate::db::AccountType as DbAccountType;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AccountType {
Asset,
Equity,
Expense,
Liability,
Revenue,
}
impl From<DbAccountType> for AccountType {
fn from(other: DbAccountType) -> Self {
match other {
DbAccountType::Asset => Self::Asset,
DbAccountType::Equity => Self::Equity,
DbAccountType::Expense => Self::Expense,
DbAccountType::Liability => Self::Liability,
DbAccountType::Revenue => Self::Revenue,
}
}
}
impl Into<DbAccountType> for AccountType {
fn into(self) -> DbAccountType {
match self {
Self::Asset => DbAccountType::Asset,
Self::Equity => DbAccountType::Equity,
Self::Expense => DbAccountType::Expense,
Self::Liability => DbAccountType::Liability,
Self::Revenue => DbAccountType::Revenue,
}
}
}

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
use crate::db::AccountType;
#[derive(Debug, Deserialize)]
pub struct AccountCreationRequest {
pub r#type: AccountType,
pub name: String,
pub description: Option<String>,
pub currency_code: String,
}

View file

@ -0,0 +1,44 @@
use serde::Serialize;
use crate::db::{AccountEntity, PermissionEntity};
use super::AccountType;
#[derive(Debug, Serialize)]
pub struct AccountCreationResponse {
pub id: i32,
pub r#type: AccountType,
pub name: String,
pub description: Option<String>,
pub currency_code: String,
pub permissions: i32,
}
impl From<(AccountEntity, Vec<PermissionEntity>)> for AccountCreationResponse {
fn from(other: (AccountEntity, Vec<PermissionEntity>)) -> Self {
let (account_entity, permissions) = other;
let AccountEntity {
id,
account_type,
name,
description,
currency_code,
..
} = account_entity;
let permissions = permissions
.into_iter()
.map(|permission| permission.value)
.sum();
Self {
id,
r#type: AccountType::from(account_type),
name,
description,
currency_code,
permissions,
}
}
}

View file

@ -0,0 +1,15 @@
mod create;
use axum::{routing::post, Router};
use create::account_creation_post_handler;
use super::AppState;
pub fn requests(state: AppState) -> Router {
Router::new().nest(
"/account",
Router::new()
.route("/", post(account_creation_post_handler))
.with_state(state),
)
}

View file

@ -1,3 +1,4 @@
mod account;
mod auth; mod auth;
mod user; mod user;
@ -94,6 +95,7 @@ pub async fn start_app(
let app = Router::new() let app = Router::new()
.merge(user::requests(state.clone())) .merge(user::requests(state.clone()))
.merge(auth::requests(state.clone())) .merge(auth::requests(state.clone()))
.merge(account::requests(state.clone()))
.layer(logging_layer); .layer(logging_layer);
info!("API started successfully."); info!("API started successfully.");

View file

@ -1,23 +1,27 @@
use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Duration}; use std::{collections::HashMap, hash::Hash, time::Duration};
use bb8_redis::{ use deadpool_redis::{
bb8::{Pool, PooledConnection}, Config, Connection, Pool,
RedisConnectionManager, redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo, ToRedisArgs, Value},
}; };
use redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo, ToRedisArgs, Value}; use tracing::error;
use crate::models::AppError; use crate::models::AppError;
pub type CachePool = Pool<RedisConnectionManager>; pub type CachePool = Pool;
pub async fn create_cache_connection_pool( pub fn create_cache_connection_pool(
connection_info: impl IntoConnectionInfo + Clone + Debug, connection_info: impl IntoConnectionInfo,
) -> Result<CachePool, AppError> { ) -> Result<CachePool, AppError> {
let manager = RedisConnectionManager::new(connection_info) let config = Config::from_connection_info(
.map_err(|_| AppError::connection_info("cache"))?; connection_info
let pool = Pool::builder().build(manager).await.unwrap(); .into_connection_info()
.map_err(|_| AppError::connection_info("cache"))?,
);
Ok(pool) config
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.map_err(|_| AppError::connection_info("cache"))
} }
pub async fn store_object< pub async fn store_object<
@ -90,8 +94,10 @@ pub async fn exists(cache_pool: &CachePool, key: &str) -> Result<bool, AppError>
conn.exists(key).await.map_err(Into::into) conn.exists(key).await.map_err(Into::into)
} }
async fn get_connection_from_pool( async fn get_connection_from_pool(cache_pool: &CachePool) -> Result<Connection, AppError> {
cache_pool: &CachePool, cache_pool
) -> Result<PooledConnection<'_, RedisConnectionManager>, AppError> { .get()
cache_pool.get().await.map_err(From::from) .await
.inspect_err(|err| error!(?err))
.map_err(From::from)
} }

View file

@ -1,7 +1,7 @@
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use deadpool_redis::redis::FromRedisValue;
use humantime::parse_rfc3339; use humantime::parse_rfc3339;
use redis::FromRedisValue;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{