2025-04-06 20:19:50 +02:00

200 lines
6.2 KiB
Rust

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<bool, tokio_postgres::Error> {
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<Uuid, Error> {
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<Option<(String, User)>, 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<Option<User>, 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<Pagination<User>, 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<Option<User>, 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<Vec<String>, 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<Option<serde_json::Value>, 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(())
}
}