diff --git a/api/Cargo.toml b/api/Cargo.toml index 2e73892..0c7cf6c 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "debt-pirate" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] argon2 = "0.5" @@ -11,8 +11,8 @@ axum = { version = "0.8", features = [ "ws", ] } base64 = "0.22" -bb8-redis = "0.20" blake3 = { version = "1.5", features = ["serde"] } +deadpool-redis = { version = "0.20", features = ["serde"] } futures = "0.3" http = "1.0" humantime = "2.1" @@ -28,7 +28,7 @@ lettre = { version = "0.11", default-features = false, features = [ ] } num_cpus = "1.16" pasetors = "0.7" -redis = { version = "0.28", features = ["aio"] } +rust_decimal = "1.36" serde = { version = "1.0", features = ["derive", "rc", "std"] } serde_json = "1.0" serde_with = "3.9" @@ -37,6 +37,7 @@ sqlx = { version = "0.8", features = [ "time", "postgres", "runtime-tokio", + "rust_decimal", ] } syslog-tracing = "0.3.1" time = { version = "0.3", features = ["formatting", "macros"] } diff --git a/api/migrations/20231221181946_create-tables.down.sql b/api/migrations/20231221181946_create-tables.down.sql index 0205930..bebce01 100644 --- a/api/migrations/20231221181946_create-tables.down.sql +++ b/api/migrations/20231221181946_create-tables.down.sql @@ -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_email_uniq_idx; -DROP INDEX IF EXISTS permission_name_uniq_idx; -DROP INDEX IF EXISTS account_type_name_uniq_idx; +DROP INDEX IF EXISTS permission_category_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.permission; 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.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; diff --git a/api/migrations/20231221181946_create-tables.up.sql b/api/migrations/20231221181946_create-tables.up.sql index e2e8afa..f3c7c04 100644 --- a/api/migrations/20231221181946_create-tables.up.sql +++ b/api/migrations/20231221181946_create-tables.up.sql @@ -1,51 +1,29 @@ -CREATE TABLE IF NOT EXISTS - public.status ( +CREATE TYPE + 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, - 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, password 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(), 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 public.permission ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + category permission_category 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(), 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 public.budget ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT 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(), updated_at TIMESTAMP WITH TIME ZONE NULL ); CREATE TABLE IF NOT EXISTS - public.account ( + public.transaction ( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - account_type_id INT NOT NULL REFERENCES account_type(id), - budget_id INT NOT NULL REFERENCES budget(id), - name TEXT NOT NULL, - description TEXT NULL, - currency_code TEXT NOT NULL, - status_id INT NOT NULL REFERENCES status(id) DEFAULT 1, + description TEXT NOT NULL, + budget_id INT NOT NULL REFERENCES public.budget(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 + 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(), updated_at TIMESTAMP WITH TIME ZONE NULL ); diff --git a/api/src/db/account.rs b/api/src/db/account.rs new file mode 100644 index 0000000..21802f0 --- /dev/null +++ b/api/src/db/account.rs @@ -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, + 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, + 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), 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)) +} diff --git a/api/src/db/account_type.rs b/api/src/db/account_type.rs new file mode 100644 index 0000000..efbb1ca --- /dev/null +++ b/api/src/db/account_type.rs @@ -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, +} diff --git a/api/src/db/mod.rs b/api/src/db/mod.rs index c03d04d..e751036 100644 --- a/api/src/db/mod.rs +++ b/api/src/db/mod.rs @@ -1,7 +1,18 @@ +mod account; +mod account_type; +mod permission; +mod status; 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}; -pub use user::*; use crate::models::AppError; diff --git a/api/src/db/permission.rs b/api/src/db/permission.rs new file mode 100644 index 0000000..423b294 --- /dev/null +++ b/api/src/db/permission.rs @@ -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, 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 { + 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, AppError> { + let ids = ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .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 { + 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) +} diff --git a/api/src/db/status.rs b/api/src/db/status.rs new file mode 100644 index 0000000..1189043 --- /dev/null +++ b/api/src/db/status.rs @@ -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, +} diff --git a/api/src/db/user.rs b/api/src/db/user.rs index 5325133..04b949d 100644 --- a/api/src/db/user.rs +++ b/api/src/db/user.rs @@ -30,7 +30,6 @@ pub struct UserEntity { pub id: i32, pub name: String, pub email: String, - pub status_id: i32, } pub async fn insert_new_user( @@ -43,16 +42,13 @@ pub async fn insert_new_user( name, } = 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(password) .bind(name) .fetch_one(pool).await - .map_err(|err| { - error!(?err, record = ?new_user, "Cannot insert new user record"); - - AppError::from(err) - }) + .inspect_err(|err| error!(?err, record = ?new_user, "Cannot insert new user record")) + .map_err(From::from) } #[derive(Debug, FromRow)] @@ -71,20 +67,16 @@ pub async fn get_username_and_password_by_email( .bind(email) .fetch_one(pool) .await - .map_err(|err| { - error!(?err, "Unable to find user"); - AppError::from(err) - }) + .inspect_err(|err| error!(?err, "Unable to find user")) + .map_err(From::from) } 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) .execute(pool) .await - .map_err(|err| { - error!(?err, user_id, "Error verifying user"); - AppError::from(err) - }) + .inspect_err(|err| error!(?err, user_id, "Error verifying user")) + .map_err(From::from) .map(|_| ()) } diff --git a/api/src/db/user_account_permission.rs b/api/src/db/user_account_permission.rs new file mode 100644 index 0000000..4dc118c --- /dev/null +++ b/api/src/db/user_account_permission.rs @@ -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, AppError> { + let values = get_all_permissions_by_category(pool, PermissionCategoryType::Account) + .await? + .into_iter() + .map(|permission| format!("({user_id}, {account_id}, {})", permission.id)) + .collect::>() + .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::>() + })?; + + get_many_permissions_by_id(pool, permission_ids.as_slice()).await +} diff --git a/api/src/main.rs b/api/src/main.rs index 8010fd0..1b5bd27 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -8,7 +8,7 @@ mod services; use db::{create_db_connection_pool, run_migrations}; use requests::start_app; 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 tracing::{error, info}; @@ -38,7 +38,6 @@ async fn main() { info!("Initializing cache service connection pool..."); let cache_pool = create_cache_connection_pool(config.cache_connection_uri().to_string()) - .await .inspect_err(|err| error!(?err)) .unwrap(); info!("Cache service connection pool created successfully."); diff --git a/api/src/models/error.rs b/api/src/models/error.rs index ef92e55..98e16d2 100644 --- a/api/src/models/error.rs +++ b/api/src/models/error.rs @@ -6,10 +6,9 @@ use std::{ }; use axum::response::IntoResponse; -use bb8_redis::bb8::RunError; +use deadpool_redis::{PoolError, redis::RedisError}; use http::StatusCode; -use redis::RedisError; -use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError}; +use sqlx::{Error as SqlxError, error::DatabaseError, migrate::MigrateError}; use toml::{self, de::Error as TomlError}; use tracing::trace; @@ -105,8 +104,8 @@ impl From for AppError { } } -impl From> for AppError { - fn from(other: RunError) -> Self { +impl From for AppError { + fn from(other: PoolError) -> Self { trace!(err = ?other, "Cache pool error"); Self::new(ErrorKind::Cache(other.to_string())) } diff --git a/api/src/models/session.rs b/api/src/models/session.rs index b182c99..7898ca7 100644 --- a/api/src/models/session.rs +++ b/api/src/models/session.rs @@ -1,9 +1,9 @@ use std::time::SystemTime; use axum::extract::FromRequestParts; +use deadpool_redis::redis::ToRedisArgs; use http::request::Parts; use humantime::format_rfc3339; -use redis::ToRedisArgs; use uuid::Uuid; use crate::{ diff --git a/api/src/requests/account/create/handler.rs b/api/src/requests/account/create/handler.rs new file mode 100644 index 0000000..44d3c6d --- /dev/null +++ b/api/src/requests/account/create/handler.rs @@ -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, + session: Session, + Json(request): Json, +) -> Result { + 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 { + 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()) +} diff --git a/api/src/requests/account/create/mod.rs b/api/src/requests/account/create/mod.rs new file mode 100644 index 0000000..f00e7ca --- /dev/null +++ b/api/src/requests/account/create/mod.rs @@ -0,0 +1,4 @@ +mod handler; +mod models; + +pub use handler::*; diff --git a/api/src/requests/account/create/models/mod.rs b/api/src/requests/account/create/models/mod.rs new file mode 100644 index 0000000..b0a9b9a --- /dev/null +++ b/api/src/requests/account/create/models/mod.rs @@ -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 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 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, + } + } +} diff --git a/api/src/requests/account/create/models/request.rs b/api/src/requests/account/create/models/request.rs new file mode 100644 index 0000000..235e6bc --- /dev/null +++ b/api/src/requests/account/create/models/request.rs @@ -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, + pub currency_code: String, +} diff --git a/api/src/requests/account/create/models/response.rs b/api/src/requests/account/create/models/response.rs new file mode 100644 index 0000000..1e7f65d --- /dev/null +++ b/api/src/requests/account/create/models/response.rs @@ -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, + pub currency_code: String, + pub permissions: i32, +} + +impl From<(AccountEntity, Vec)> for AccountCreationResponse { + fn from(other: (AccountEntity, Vec)) -> 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, + } + } +} diff --git a/api/src/requests/account/mod.rs b/api/src/requests/account/mod.rs new file mode 100644 index 0000000..46ebd45 --- /dev/null +++ b/api/src/requests/account/mod.rs @@ -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), + ) +} diff --git a/api/src/requests/mod.rs b/api/src/requests/mod.rs index cc7436d..bb2ea88 100644 --- a/api/src/requests/mod.rs +++ b/api/src/requests/mod.rs @@ -1,3 +1,4 @@ +mod account; mod auth; mod user; @@ -94,6 +95,7 @@ pub async fn start_app( let app = Router::new() .merge(user::requests(state.clone())) .merge(auth::requests(state.clone())) + .merge(account::requests(state.clone())) .layer(logging_layer); info!("API started successfully."); diff --git a/api/src/services/cache.rs b/api/src/services/cache.rs index 2169acc..cd6c143 100644 --- a/api/src/services/cache.rs +++ b/api/src/services/cache.rs @@ -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::{ - bb8::{Pool, PooledConnection}, - RedisConnectionManager, +use deadpool_redis::{ + Config, Connection, Pool, + redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo, ToRedisArgs, Value}, }; -use redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo, ToRedisArgs, Value}; +use tracing::error; use crate::models::AppError; -pub type CachePool = Pool; +pub type CachePool = Pool; -pub async fn create_cache_connection_pool( - connection_info: impl IntoConnectionInfo + Clone + Debug, +pub fn create_cache_connection_pool( + connection_info: impl IntoConnectionInfo, ) -> Result { - let manager = RedisConnectionManager::new(connection_info) - .map_err(|_| AppError::connection_info("cache"))?; - let pool = Pool::builder().build(manager).await.unwrap(); + let config = Config::from_connection_info( + connection_info + .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< @@ -90,8 +94,10 @@ pub async fn exists(cache_pool: &CachePool, key: &str) -> Result conn.exists(key).await.map_err(Into::into) } -async fn get_connection_from_pool( - cache_pool: &CachePool, -) -> Result, AppError> { - cache_pool.get().await.map_err(From::from) +async fn get_connection_from_pool(cache_pool: &CachePool) -> Result { + cache_pool + .get() + .await + .inspect_err(|err| error!(?err)) + .map_err(From::from) } diff --git a/api/src/services/user_session.rs b/api/src/services/user_session.rs index b430dc4..3a4bdfc 100644 --- a/api/src/services/user_session.rs +++ b/api/src/services/user_session.rs @@ -1,7 +1,7 @@ use std::time::{Duration, SystemTime}; +use deadpool_redis::redis::FromRedisValue; use humantime::parse_rfc3339; -use redis::FromRedisValue; use uuid::Uuid; use crate::{