use bank_core::{ ApiError, pagination::{Pagination, RequestPagination}, user::User, }; use deadpool_postgres::GenericClient; use tokio_postgres::{Row, error::SqlState}; use tracing::instrument; use uuid::{NoContext, Timestamp, Uuid}; use crate::api::Error; use super::{Accounts, count}; pub struct Users; pub fn user_from_row(row: Row) -> User { User { id: row.get("id"), name: row.get("name"), } } fn unique_violation(error: tokio_postgres::Error, api_error: fn() -> ApiError<'static>) -> Error { let Some(code) = error.code() else { return error.into(); }; if *code == SqlState::UNIQUE_VIOLATION { return api_error().into(); } return error.into(); } impl Users { pub async fn exists( client: &impl GenericClient, id: Uuid, ) -> Result { let stmt = client .prepare_cached("select exists(select 1 from users where id = $1)") .await?; let result = client.query_one(&stmt, &[&id]).await?; Ok(result.get(0)) } #[instrument(skip(client, password))] pub async fn create( client: &mut impl GenericClient, name: &str, password: &str, ) -> Result { let hash = password_auth::generate_hash(password); let stmt = client .prepare_cached("insert into users(id, name, password) values ($1, $2, $3)") .await?; let mut tx = client.transaction().await?; let id = Uuid::new_v7(Timestamp::now(NoContext)); tx.execute(&stmt, &[&id, &name, &hash]) .await .map_err(|err| unique_violation(err, || ApiError::CONFLICT))?; Accounts::create(&mut tx, Some(id), id, "personal") .await .map_err(|err| unique_violation(err, || ApiError::CONFLICT))?; tx.commit().await?; Ok(id) } #[instrument(skip(client, password))] pub async fn change_password( client: &mut impl GenericClient, user: Uuid, password: &str, ) -> Result<(), tokio_postgres::Error> { let hash = password_auth::generate_hash(password); let stmt = client .prepare_cached("update users set password = $2 where id = $1") .await?; client.execute(&stmt, &[&user, &hash]).await?; Ok(()) } #[instrument(skip(client))] pub async fn get_password_and_info_by_username( client: &impl GenericClient, name: &str, ) -> Result, tokio_postgres::Error> { let stmt = client .prepare_cached("select id,name,password from users where name = $1") .await?; let res = client.query_opt(&stmt, &[&name]).await?; Ok(res.map(|res| { let password = res.get("password"); (password, user_from_row(res)) })) } #[instrument(skip(client))] pub async fn info_by_name( client: &impl GenericClient, name: &str, ) -> Result, tokio_postgres::Error> { let stmt = client .prepare_cached("select id,name from users where name = $1") .await?; let res = client.query_opt(&stmt, &[&name]).await?; Ok(res.map(user_from_row)) } #[instrument(skip(client))] pub async fn list( client: &impl GenericClient, pagination: RequestPagination, ) -> Result, tokio_postgres::Error> { let stmt = client .prepare_cached("select id,name from users limit $1 offset $2") .await?; let stmt_count = client.prepare_cached("select count(*) from users").await?; let users = client .query(&stmt, &[&pagination.limit(), &pagination.offset()]) .await? .into_iter() .map(user_from_row) .collect(); let count = count(client, &stmt_count, &[]).await?; Ok(Pagination::new(users, count, pagination)) } #[instrument(skip(client))] pub async fn info( client: &impl GenericClient, id: Uuid, ) -> Result, tokio_postgres::Error> { let stmt = client .prepare_cached("select id,name from users where id = $1") .await?; let info = client.query_opt(&stmt, &[&id]).await?.map(user_from_row); Ok(info) } #[instrument(skip(client))] pub async fn list_data( client: &impl GenericClient, user: Uuid, ) -> Result, tokio_postgres::Error> { let stmt = client .prepare_cached("select \"key\" from user_data where owner = $1 and \"user\" = $1") .await?; let res = client .query(&stmt, &[&user]) .await? .into_iter() .map(|row| row.get(0)) .collect(); Ok(res) } #[instrument(skip(client))] pub async fn get_data( client: &impl GenericClient, user: Uuid, key: &str, ) -> Result, tokio_postgres::Error> { let stmt = client .prepare_cached( "select data from user_data where owner = $1 and \"user\" = $1 and \"key\" = $2 ", ) .await?; let res = client.query_opt(&stmt, &[&user, &key]).await?; Ok(res.map(|row| row.get(0))) } #[instrument(skip(client, data))] pub async fn set_data( client: &mut impl GenericClient, user: Uuid, key: &str, data: serde_json::Value, ) -> Result<(), tokio_postgres::Error> { let stmt = client .prepare_cached("insert into user_data(owner, \"user\", \"key\", data) values ($1, $1, $2, $3) on conflict (owner, \"user\") do update set data = $3") .await?; client.execute(&stmt, &[&user, &key, &data]).await?; Ok(()) } #[instrument(skip(client))] pub async fn delete_data( client: &mut impl GenericClient, user: Uuid, key: &str, ) -> Result<(), tokio_postgres::Error> { let stmt = client .prepare_cached( "delete from user_data where \"owner\" = $1 and \"user\" = $1 and \"key\" = $2", ) .await?; client.execute(&stmt, &[&user, &key]).await?; Ok(()) } }