mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 02:59:20 +01:00
split into bank_core and server
This commit is contained in:
parent
0e90a4e2e4
commit
2d3cc8edc1
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -173,12 +173,28 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bank_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"chrono",
|
||||||
|
"garde",
|
||||||
|
"http",
|
||||||
|
"regex",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bankserver"
|
name = "bankserver"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"axum",
|
"axum",
|
||||||
|
"bank_core",
|
||||||
"chrono",
|
"chrono",
|
||||||
"concread",
|
"concread",
|
||||||
"config",
|
"config",
|
||||||
@ -722,9 +738,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.2.0"
|
version = "1.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
|
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@ -1415,9 +1431,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_path_to_error"
|
name = "serde_path_to_error"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
|
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
35
Cargo.toml
35
Cargo.toml
@ -1,3 +1,17 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [ "bank_core" ]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
chrono = { version = "0.4.40", features = ["serde"] }
|
||||||
|
garde = { version = "0.22.0", features = ["serde", "derive", "regex", "pattern"] }
|
||||||
|
http = "1.2"
|
||||||
|
regex = "1.11.1"
|
||||||
|
schemars = { version = "1.0.0-alpha.17", features = ["chrono04", "uuid1"] }
|
||||||
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
|
serde_json = "1.0.139"
|
||||||
|
uuid = { version = "1.15.1", features = ["serde", "v7"] }
|
||||||
|
axum = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "bankserver"
|
name = "bankserver"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -5,33 +19,34 @@ edition = "2024"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["schemas"]
|
default = ["schemas"]
|
||||||
schemas = ["dep:schemars"]
|
schemas = ["dep:schemars", "bank_core/schemas"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "generate-schemas"
|
name = "generate-schemas"
|
||||||
features = ["schemas"]
|
required-features = ["schemas"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-broadcast = "0.7.2"
|
async-broadcast = "0.7.2"
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { workspace = true, features = ["form", "http1", "json", "matched-path", "query", "tokio", "tower-log", "tracing", "ws"] }
|
||||||
chrono = { version = "0.4.40", features = ["serde"] }
|
chrono.workspace = true
|
||||||
concread = { version = "0.5.4", default-features = false, features = ["ahash", "asynch", "maps"] }
|
concread = { version = "0.5.4", default-features = false, features = ["ahash", "asynch", "maps"] }
|
||||||
config = { version = "0.15.8", default-features = false }
|
config = { version = "0.15.8", default-features = false }
|
||||||
dbmigrator = { git = "https://github.com/DSeeLP/dbmigrator.git", branch = "macros", version = "0.4.4-alpha", features = ["tokio-postgres"] }
|
dbmigrator = { git = "https://github.com/DSeeLP/dbmigrator.git", branch = "macros", version = "0.4.4-alpha", features = ["tokio-postgres"] }
|
||||||
deadpool = "0.12"
|
deadpool = "0.12"
|
||||||
deadpool-postgres = { version = "0.14", features = ["serde"] }
|
deadpool-postgres = { version = "0.14", features = ["serde"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
garde = { version = "0.22.0", features = ["serde", "derive", "regex", "pattern"] }
|
garde.workspace = true
|
||||||
jsonwebtoken = { version = "9.3", default-features = false }
|
jsonwebtoken = { version = "9.3", default-features = false }
|
||||||
password-auth = "1.0.0"
|
password-auth = "1.0.0"
|
||||||
regex = "1.11.1"
|
regex.workspace = true
|
||||||
schemars = { version = "1.0.0-alpha.17", optional = true, features = ["chrono04", "uuid1"] }
|
schemars = { workspace = true, optional = true }
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
serde.workspace = true
|
||||||
serde_json = "1.0.139"
|
serde_json.workspace = true
|
||||||
serde_with = "3.12.0"
|
serde_with = "3.12.0"
|
||||||
tokio = { version = "1.43", features = ["tracing", "time", "sync", "net", "io-std", "io-util", "macros", "rt-multi-thread", "signal"] }
|
tokio = { version = "1.43", features = ["tracing", "time", "sync", "net", "io-std", "io-util", "macros", "rt-multi-thread", "signal"] }
|
||||||
tokio-postgres = { version = "0.7.13", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] }
|
tokio-postgres = { version = "0.7.13", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-error = "0.2.1"
|
tracing-error = "0.2.1"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
uuid = { version = "1.15.1", features = ["serde", "v7"] }
|
uuid.workspace = true
|
||||||
|
bank_core = { path = "./bank_core", features = ["axum"] }
|
||||||
|
|||||||
19
bank_core/Cargo.toml
Normal file
19
bank_core/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "bank_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
schemas = ["dep:schemars"]
|
||||||
|
axum = ["dep:axum"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { workspace = true, optional = true }
|
||||||
|
chrono.workspace = true
|
||||||
|
garde.workspace = true
|
||||||
|
http.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
|
schemars = { workspace = true, optional = true }
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
47
bank_core/src/account.rs
Normal file
47
bank_core/src/account.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
make_schemas,
|
||||||
|
pagination::{Pagination, PaginationType},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Account {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub user: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub balance: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct AccountInfo {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub user: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct UserAccountInfo {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub balance: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ReducedAccountInfo {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaginationType for UserAccountInfo {
|
||||||
|
const NAME: &str = "UserAccounts";
|
||||||
|
}
|
||||||
|
impl PaginationType for AccountInfo {
|
||||||
|
const NAME: &str = "Accounts";
|
||||||
|
}
|
||||||
|
|
||||||
|
make_schemas!((); (Account, AccountInfo, UserAccountInfo, Pagination<AccountInfo>, Pagination<UserAccountInfo>));
|
||||||
58
bank_core/src/chat.rs
Normal file
58
bank_core/src/chat.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
NameOrUuid, make_schemas,
|
||||||
|
pagination::{Pagination, PaginationType},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Chat {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ChatInfo {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub read_until: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ChatMessage {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub sender: Uuid,
|
||||||
|
pub time: DateTime<Utc>,
|
||||||
|
pub text: String,
|
||||||
|
pub extra: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaginationType for Chat {
|
||||||
|
const NAME: &str = "Chats";
|
||||||
|
}
|
||||||
|
impl PaginationType for ChatInfo {
|
||||||
|
const NAME: &str = "ChatInfos";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaginationType for ChatMessage {
|
||||||
|
const NAME: &str = "ChatMessages";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct SendMessage {
|
||||||
|
pub text: String,
|
||||||
|
pub extra: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct StartChat {
|
||||||
|
pub user: NameOrUuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
make_schemas!((StartChat, SendMessage); (Chat, ChatInfo, ChatMessage, Pagination<Chat>, Pagination<ChatInfo>, Pagination<ChatMessage>));
|
||||||
61
bank_core/src/error.rs
Normal file
61
bank_core/src/error.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use http::StatusCode;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct ApiError<'a> {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub status: StatusCode,
|
||||||
|
pub id: Cow<'a, str>,
|
||||||
|
pub message: Cow<'a, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
impl<'a> axum::response::IntoResponse for ApiError<'a> {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(self.status, axum::Json(self)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ApiError<'a> {
|
||||||
|
pub const NOT_FOUND: ApiError<'static> =
|
||||||
|
ApiError::const_new(StatusCode::NOT_FOUND, "not-found", "Not found");
|
||||||
|
pub const INTERNAL_SERVER_ERROR: ApiError<'static> = ApiError::const_new(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"internal-server-error",
|
||||||
|
"Internal Server Error",
|
||||||
|
);
|
||||||
|
pub const FORBIDDEN: ApiError<'static> =
|
||||||
|
ApiError::const_new(StatusCode::FORBIDDEN, "forbidden", "Forbidden");
|
||||||
|
|
||||||
|
pub const CONFLICT: ApiError<'static> =
|
||||||
|
ApiError::const_new(StatusCode::CONFLICT, "conflict", "Conflict");
|
||||||
|
pub const fn const_new(status: StatusCode, id: &'static str, message: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
id: Cow::Borrowed(id),
|
||||||
|
message: Cow::Borrowed(message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new(
|
||||||
|
status: StatusCode,
|
||||||
|
id: impl Into<Cow<'a, str>>,
|
||||||
|
message: impl Into<Cow<'a, str>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
id: id.into(),
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn into_static(self) -> ApiError<'static> {
|
||||||
|
ApiError {
|
||||||
|
status: self.status,
|
||||||
|
id: Cow::Owned(self.id.into_owned()),
|
||||||
|
message: Cow::Owned(self.message.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
bank_core/src/lib.rs
Normal file
13
bank_core/src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
pub mod account;
|
||||||
|
pub mod chat;
|
||||||
|
pub mod pagination;
|
||||||
|
mod schemas;
|
||||||
|
pub mod transaction;
|
||||||
|
pub mod user;
|
||||||
|
pub use schemas::*;
|
||||||
|
mod error;
|
||||||
|
mod util;
|
||||||
|
pub use error::*;
|
||||||
|
pub use util::*;
|
||||||
|
|
||||||
|
make_schemas!((Credentials); (ApiError, TokenResponse), [account::schemas, chat::schemas, transaction::schemas, user::schemas]);
|
||||||
152
bank_core/src/pagination.rs
Normal file
152
bank_core/src/pagination.rs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
pub trait PaginationType {
|
||||||
|
const NAME: &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct RequestPagination {
|
||||||
|
#[serde(deserialize_with = "deserialize_u64")]
|
||||||
|
pub limit: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(deserialize_with = "deserialize_u64")]
|
||||||
|
pub offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
#[serde(rename = "Pagination")]
|
||||||
|
pub struct PaginationMeta {
|
||||||
|
pub total: u64,
|
||||||
|
pub limit: u64,
|
||||||
|
pub offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Pagination<T> {
|
||||||
|
pub pagination: PaginationMeta,
|
||||||
|
pub result: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Pagination<T> {
|
||||||
|
pub fn new(result: Vec<T>, total: u64, pagination: RequestPagination) -> Self {
|
||||||
|
Self {
|
||||||
|
pagination: PaginationMeta {
|
||||||
|
total,
|
||||||
|
limit: pagination.limit,
|
||||||
|
offset: pagination.offset,
|
||||||
|
},
|
||||||
|
result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "schemas")]
|
||||||
|
impl<T: PaginationType + schemars::JsonSchema> schemars::JsonSchema for Pagination<T> {
|
||||||
|
fn schema_name() -> std::borrow::Cow<'static, str> {
|
||||||
|
format!("Paginated{}", T::NAME).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema_id() -> std::borrow::Cow<'static, str> {
|
||||||
|
std::borrow::Cow::Borrowed(std::any::type_name::<Self>())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
|
let result_schema = generator.subschema_for::<Vec<T>>();
|
||||||
|
let pagination_schema = generator.subschema_for::<PaginationMeta>();
|
||||||
|
schemars::Schema::try_from(serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pagination": pagination_schema,
|
||||||
|
"result": result_schema
|
||||||
|
},
|
||||||
|
"required": ["pagination", "result"]
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! visit {
|
||||||
|
(signed $ty:ident : $visit:ident) => {
|
||||||
|
#[inline]
|
||||||
|
fn $visit<E>(self, v: $ty) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
if 0 <= v && v as u64 <= Self::Value::max_value() as u64 {
|
||||||
|
Ok(v as Self::Value)
|
||||||
|
} else {
|
||||||
|
Err(E::invalid_value(
|
||||||
|
serde::de::Unexpected::Signed(v as i64),
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($ty:ident : $visit:ident) => {
|
||||||
|
#[inline]
|
||||||
|
fn $visit<E>(self, v: $ty) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
if v as u64 <= Self::Value::max_value() as u64 {
|
||||||
|
Ok(v as Self::Value)
|
||||||
|
} else {
|
||||||
|
Err(E::invalid_value(
|
||||||
|
serde::de::Unexpected::Unsigned(v as u64),
|
||||||
|
&self,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct Helper;
|
||||||
|
impl<'a> serde::de::Visitor<'a> for Helper {
|
||||||
|
type Value = u64;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a u64")
|
||||||
|
}
|
||||||
|
|
||||||
|
visit!(signed i8: visit_i8);
|
||||||
|
visit!(signed i16: visit_i16);
|
||||||
|
visit!(signed i32: visit_i32);
|
||||||
|
visit!(signed i64: visit_i64);
|
||||||
|
|
||||||
|
visit!(u8: visit_u8);
|
||||||
|
visit!(u16: visit_u16);
|
||||||
|
visit!(u32: visit_u32);
|
||||||
|
visit!(u64: visit_u64);
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
match v.parse() {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(_) => Err(E::invalid_value(
|
||||||
|
serde::de::Unexpected::Str(v),
|
||||||
|
&"valid unsigned integer",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deserializer.deserialize_any(Helper)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestPagination {
|
||||||
|
#[inline]
|
||||||
|
pub const fn limit(&self) -> i64 {
|
||||||
|
self.limit as i64
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub const fn offset(&self) -> i64 {
|
||||||
|
self.offset as i64
|
||||||
|
}
|
||||||
|
}
|
||||||
95
bank_core/src/schemas.rs
Normal file
95
bank_core/src/schemas.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! make_schemas {
|
||||||
|
(($($request_bodies:ty),*); ($($response_bodies:ty),*)$(, [$($deps:expr),*])? ) => {
|
||||||
|
#[cfg(feature = "schemas")]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub const fn schemas() -> $crate::Schemas {
|
||||||
|
fn request_bodies(generator: &mut schemars::SchemaGenerator, schemas: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {
|
||||||
|
$(
|
||||||
|
generator.subschema_for::<$request_bodies>();
|
||||||
|
// schemas.insert(<$request_bodies as schemars::JsonSchema>::schema_name(), generator.root_schema_for::<$request_bodies>());
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
fn response_bodies(generator: &mut schemars::SchemaGenerator, schemas: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {
|
||||||
|
$(
|
||||||
|
generator.subschema_for::<$response_bodies>();
|
||||||
|
// schemas.insert(<$response_bodies as schemars::JsonSchema>::schema_name(), generator.root_schema_for::<$response_bodies>());
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
static DEPS: &'static [$crate::Schemas] = &[$($($deps()),*)?];
|
||||||
|
$crate::Schemas {
|
||||||
|
request_bodies,
|
||||||
|
response_bodies,
|
||||||
|
contains: DEPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "schemas"))]
|
||||||
|
pub const fn schemas() -> $crate::Schemas {
|
||||||
|
#[allow(unused)]
|
||||||
|
const fn test<T>() {}
|
||||||
|
$(
|
||||||
|
test::<$request_bodies>();
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
test::<$response_bodies>();
|
||||||
|
)*
|
||||||
|
const _DEPS: &'static [$crate::Schemas] = &[$($($deps()),*)?];
|
||||||
|
$crate::Schemas
|
||||||
|
}
|
||||||
|
};
|
||||||
|
([$($deps:expr),*]) => {
|
||||||
|
#[cfg(feature = "schemas")]
|
||||||
|
pub const fn schemas() -> $crate::Schemas {
|
||||||
|
fn ident(_: &mut schemars::SchemaGenerator, _: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {}
|
||||||
|
static DEPS: &'static [$crate::Schemas] = &[$($deps()),*];
|
||||||
|
$crate::Schemas {
|
||||||
|
request_bodies: ident,
|
||||||
|
response_bodies: ident,
|
||||||
|
contains: DEPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "schemas"))]
|
||||||
|
pub const fn schemas() -> $crate::Schemas {
|
||||||
|
const _DEPS: &'static [$crate::Schemas] = &[$($deps()),*];
|
||||||
|
$crate::Schemas
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "schemas"))]
|
||||||
|
pub struct Schemas;
|
||||||
|
#[cfg(feature = "schemas")]
|
||||||
|
pub struct Schemas {
|
||||||
|
pub request_bodies: fn(
|
||||||
|
&mut schemars::SchemaGenerator,
|
||||||
|
&mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>,
|
||||||
|
),
|
||||||
|
pub response_bodies: fn(
|
||||||
|
&mut schemars::SchemaGenerator,
|
||||||
|
&mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>,
|
||||||
|
),
|
||||||
|
pub contains: &'static [Self],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "schemas")]
|
||||||
|
impl Schemas {
|
||||||
|
pub fn generate(
|
||||||
|
&self,
|
||||||
|
requests: &mut schemars::SchemaGenerator,
|
||||||
|
responses: &mut schemars::SchemaGenerator,
|
||||||
|
request_schemas: &mut std::collections::HashMap<
|
||||||
|
std::borrow::Cow<'static, str>,
|
||||||
|
schemars::Schema,
|
||||||
|
>,
|
||||||
|
response_schemas: &mut std::collections::HashMap<
|
||||||
|
std::borrow::Cow<'static, str>,
|
||||||
|
schemars::Schema,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for schemas in self.contains {
|
||||||
|
schemas.generate(requests, responses, request_schemas, response_schemas);
|
||||||
|
}
|
||||||
|
(self.request_bodies)(requests, request_schemas);
|
||||||
|
(self.response_bodies)(requests, response_schemas);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
bank_core/src/transaction.rs
Normal file
58
bank_core/src/transaction.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::ReducedAccountInfo,
|
||||||
|
make_schemas,
|
||||||
|
pagination::{Pagination, PaginationType, RequestPagination},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Transaction {
|
||||||
|
pub from: Uuid,
|
||||||
|
pub to: Uuid,
|
||||||
|
pub amount: u64,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct FullTransaction {
|
||||||
|
pub from: Participant,
|
||||||
|
pub to: Participant,
|
||||||
|
pub amount: u64,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Direction {
|
||||||
|
Received,
|
||||||
|
Sent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Participant {
|
||||||
|
pub user: User,
|
||||||
|
pub account: ReducedAccountInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct TransactionQuery {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub pagination: RequestPagination,
|
||||||
|
pub direction: Option<Direction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaginationType for FullTransaction {
|
||||||
|
const NAME: &str = "Transactions";
|
||||||
|
}
|
||||||
|
|
||||||
|
make_schemas!((Direction); (Transaction, FullTransaction, Pagination<FullTransaction>));
|
||||||
33
bank_core/src/user.rs
Normal file
33
bank_core/src/user.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::UserAccountInfo,
|
||||||
|
make_schemas,
|
||||||
|
pagination::{Pagination, PaginationType},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct User {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaginationType for User {
|
||||||
|
const NAME: &str = "UserList";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct UserAccounts {
|
||||||
|
pub result: Vec<UserAccountInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct UserBalance {
|
||||||
|
pub balance: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
make_schemas!((); (User, Pagination<User>, UserAccounts, UserBalance));
|
||||||
65
bank_core/src/util.rs
Normal file
65
bank_core/src/util.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use std::{ops::Deref, sync::LazyLock};
|
||||||
|
|
||||||
|
use garde::Validate;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
static NAME_PATTERN_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(NAME_PATTERN).unwrap());
|
||||||
|
|
||||||
|
pub const NAME_PATTERN: &str = "^[a-z0-9_-]+$";
|
||||||
|
pub const USER_ACCOUNT_PATTERN: &str = "([a-z0-9_-]+):([a-z0-9_-]+)";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Validate, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
#[garde(transparent)]
|
||||||
|
pub struct Name(#[garde(length(min = 3, max = 32), pattern(NAME_PATTERN_RE))] pub String);
|
||||||
|
|
||||||
|
impl Deref for Name {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Name {
|
||||||
|
pub fn into_inner(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_name(name: &str) -> Result<(), garde::Error> {
|
||||||
|
garde::rules::pattern::apply(&name, (&NAME_PATTERN_RE,))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Validate, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum NameOrUuid {
|
||||||
|
Id(#[garde(skip)] Uuid),
|
||||||
|
Name(#[garde(dive)] Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Validate, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Credentials {
|
||||||
|
#[garde(dive)]
|
||||||
|
pub name: Name,
|
||||||
|
#[garde(length(min = 8, max = 96))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Credentials {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Credentials")
|
||||||
|
.field("name", &self.name)
|
||||||
|
.field("password", &"...")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
|
pub struct TokenResponse {
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
@ -1,18 +1,18 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{Router, extract::Path, routing::get};
|
use axum::{Router, extract::Path, routing::get};
|
||||||
|
use bank_core::{
|
||||||
|
ApiError,
|
||||||
|
account::AccountInfo,
|
||||||
|
pagination::Pagination,
|
||||||
|
transaction::{FullTransaction, TransactionQuery},
|
||||||
|
};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::model::{Accounts, Transactions};
|
||||||
api::{ApiError, Pagination},
|
|
||||||
model::{Account, AccountInfo, FullTransaction, Transaction},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{AppState, EState, Error, Json, PaginationQuery, Query, State, auth::Auth};
|
||||||
AppState, EState, Error, Json, Query, RequestPagination, State, auth::Auth, make_schemas,
|
|
||||||
transactions::TransactionQuery,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
@ -21,8 +21,6 @@ pub(super) fn router() -> Router<Arc<AppState>> {
|
|||||||
.route("/{id}/transactions", get(account_transactions))
|
.route("/{id}/transactions", get(account_transactions))
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((); (Pagination<AccountInfo>));
|
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn account_info(EState(state): State, _: Auth, Path(id): Path<Uuid>) {}
|
pub async fn account_info(EState(state): State, _: Auth, Path(id): Path<Uuid>) {}
|
||||||
|
|
||||||
@ -37,14 +35,14 @@ pub async fn account_transactions(
|
|||||||
}): Query<TransactionQuery>,
|
}): Query<TransactionQuery>,
|
||||||
) -> Result<Json<Pagination<FullTransaction>>, Error> {
|
) -> Result<Json<Pagination<FullTransaction>>, Error> {
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
match Account::owned_by(&conn, id, auth.user_id()).await? {
|
match Accounts::owned_by(&conn, id, auth.user_id()).await? {
|
||||||
Some(false) => {
|
Some(false) => {
|
||||||
return Err(ApiError::FORBIDDEN.into());
|
return Err(ApiError::FORBIDDEN.into());
|
||||||
}
|
}
|
||||||
None => return Err(ApiError::NOT_FOUND.into()),
|
None => return Err(ApiError::NOT_FOUND.into()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let result = Transaction::account_history(&conn, id, direction, pagination).await?;
|
let result = Transactions::account_history(&conn, id, direction, pagination).await?;
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
@ -53,9 +51,9 @@ pub async fn account_transactions(
|
|||||||
pub async fn list_accounts(
|
pub async fn list_accounts(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
_: Auth,
|
_: Auth,
|
||||||
pagination: RequestPagination,
|
PaginationQuery(pagination): PaginationQuery,
|
||||||
) -> Result<Json<Pagination<AccountInfo>>, Error> {
|
) -> Result<Json<Pagination<AccountInfo>>, Error> {
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let result = Account::list_all(&conn, pagination).await?;
|
let result = Accounts::list_all(&conn, pagination).await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,23 +6,20 @@ use std::{
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::{FromRef, FromRequestParts},
|
extract::FromRequestParts,
|
||||||
http::{StatusCode, request::Parts},
|
http::{StatusCode, request::Parts},
|
||||||
routing::post,
|
routing::post,
|
||||||
};
|
};
|
||||||
use garde::Validate;
|
use bank_core::{ApiError, Credentials, TokenResponse};
|
||||||
use jsonwebtoken::{Algorithm, Header, Validation};
|
use jsonwebtoken::{Algorithm, Header, Validation};
|
||||||
use password_auth::verify_password;
|
use password_auth::verify_password;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{api::InnerError, model::Users};
|
||||||
api::{ApiError, InnerError},
|
|
||||||
model::{Name, User},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{AppState, EState, Error, Json, State, make_schemas};
|
use super::{AppState, EState, Error, Json, State};
|
||||||
|
|
||||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
@ -30,33 +27,13 @@ pub(super) fn router() -> Router<Arc<AppState>> {
|
|||||||
.route("/login", post(login))
|
.route("/login", post(login))
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((Credentials); (TokenResponse));
|
|
||||||
|
|
||||||
#[derive(Deserialize, Validate)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct Credentials {
|
|
||||||
#[garde(dive)]
|
|
||||||
pub name: Name,
|
|
||||||
#[garde(length(min = 8, max = 96))]
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Credentials {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("Credentials")
|
|
||||||
.field("name", &self.name)
|
|
||||||
.field("password", &"...")
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
async fn register(
|
async fn register(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
Json(credentials): Json<Credentials>,
|
Json(credentials): Json<Credentials>,
|
||||||
) -> Result<(StatusCode, Json<TokenResponse>), Error> {
|
) -> Result<(StatusCode, Json<TokenResponse>), Error> {
|
||||||
let mut conn = state.conn().await?;
|
let mut conn = state.conn().await?;
|
||||||
let id = User::create(&mut conn, &credentials.name, &credentials.password).await?;
|
let id = Users::create(&mut conn, &credentials.name, &credentials.password).await?;
|
||||||
let token = Claims::new(id).encode(&state.encoding_key).unwrap();
|
let token = Claims::new(id).encode(&state.encoding_key).unwrap();
|
||||||
Ok((StatusCode::CREATED, Json(TokenResponse { token })))
|
Ok((StatusCode::CREATED, Json(TokenResponse { token })))
|
||||||
}
|
}
|
||||||
@ -68,7 +45,7 @@ async fn login(
|
|||||||
) -> Result<Json<TokenResponse>, Error> {
|
) -> Result<Json<TokenResponse>, Error> {
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let Some((hash, info)) =
|
let Some((hash, info)) =
|
||||||
User::get_password_and_info_by_username(&conn, &credentials.name).await?
|
Users::get_password_and_info_by_username(&conn, &credentials.name).await?
|
||||||
else {
|
else {
|
||||||
return Err(invalid_username_or_password().into());
|
return Err(invalid_username_or_password().into());
|
||||||
};
|
};
|
||||||
@ -82,12 +59,6 @@ async fn login(
|
|||||||
Ok(Json(TokenResponse { token: jwt }))
|
Ok(Json(TokenResponse { token: jwt }))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct TokenResponse {
|
|
||||||
token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct Claims<'a> {
|
struct Claims<'a> {
|
||||||
#[serde(rename = "iss")]
|
#[serde(rename = "iss")]
|
||||||
|
|||||||
@ -1,22 +1,21 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{Router, extract::Path, routing::get};
|
use axum::{Router, extract::Path, routing::get};
|
||||||
|
use bank_core::{
|
||||||
|
ApiError,
|
||||||
|
chat::{Chat, ChatInfo, ChatMessage, SendMessage, StartChat},
|
||||||
|
pagination::Pagination,
|
||||||
|
};
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use serde::Deserialize;
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::model::{Chats, NameOrUuidExt};
|
||||||
api::ApiError,
|
|
||||||
model::{Chat, ChatInfo, ChatMessage},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppState, EState, Error, Json, Pagination, RequestPagination, State,
|
AppState, EState, Error, Json, PaginationQuery, State,
|
||||||
auth::Auth,
|
auth::Auth,
|
||||||
make_schemas,
|
|
||||||
socket::{SocketEvent, SocketMessage},
|
socket::{SocketEvent, SocketMessage},
|
||||||
transactions::NameOrUuid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
@ -26,32 +25,17 @@ pub(super) fn router() -> Router<Arc<AppState>> {
|
|||||||
.route("/{id}/messages", get(get_messages).post(send_message))
|
.route("/{id}/messages", get(get_messages).post(send_message))
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((StartChat, SendMessage); (Pagination<Chat>, Pagination<ChatInfo>, Pagination<ChatMessage>));
|
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn list_chats(
|
pub async fn list_chats(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
pagination: RequestPagination,
|
PaginationQuery(pagination): PaginationQuery,
|
||||||
) -> Result<Json<Pagination<Chat>>, Error> {
|
) -> Result<Json<Pagination<Chat>>, Error> {
|
||||||
let client = state.conn().await?;
|
let client = state.conn().await?;
|
||||||
let chats = Chat::list_for_user(&client, auth.user_id(), pagination).await?;
|
let chats = Chats::list_for_user(&client, auth.user_id(), pagination).await?;
|
||||||
Ok(Json(chats))
|
Ok(Json(chats))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct SendMessage {
|
|
||||||
pub text: String,
|
|
||||||
pub extra: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct StartChat {
|
|
||||||
pub user: NameOrUuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn create_chat(
|
pub async fn create_chat(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
@ -62,7 +46,7 @@ pub async fn create_chat(
|
|||||||
let Some(target) = body.user.user_id(&client).await? else {
|
let Some(target) = body.user.user_id(&client).await? else {
|
||||||
return Err(ApiError::NOT_FOUND.into());
|
return Err(ApiError::NOT_FOUND.into());
|
||||||
};
|
};
|
||||||
let chat = Chat::start(&mut client, auth.user_id(), target).await?;
|
let chat = Chats::start(&mut client, auth.user_id(), target).await?;
|
||||||
Ok(Json(chat))
|
Ok(Json(chat))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +58,7 @@ pub async fn get_chat(
|
|||||||
) -> Result<Json<ChatInfo>, Error> {
|
) -> Result<Json<ChatInfo>, Error> {
|
||||||
let client = state.conn().await?;
|
let client = state.conn().await?;
|
||||||
check_chat(&client, id, auth.user_id()).await?;
|
check_chat(&client, id, auth.user_id()).await?;
|
||||||
let Some(chat) = Chat::get_by_id_for_user(&client, id, auth.user_id()).await? else {
|
let Some(chat) = Chats::get_by_id_for_user(&client, id, auth.user_id()).await? else {
|
||||||
return Err(ApiError::NOT_FOUND.into());
|
return Err(ApiError::NOT_FOUND.into());
|
||||||
};
|
};
|
||||||
Ok(Json(chat))
|
Ok(Json(chat))
|
||||||
@ -85,16 +69,16 @@ pub async fn get_messages(
|
|||||||
EState(state): State,
|
EState(state): State,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
pagination: RequestPagination,
|
PaginationQuery(pagination): PaginationQuery,
|
||||||
) -> Result<Json<Pagination<ChatMessage>>, Error> {
|
) -> Result<Json<Pagination<ChatMessage>>, Error> {
|
||||||
let client = state.conn().await?;
|
let client = state.conn().await?;
|
||||||
check_chat(&client, id, auth.user_id()).await?;
|
check_chat(&client, id, auth.user_id()).await?;
|
||||||
let messages = Chat::messages(&client, id, pagination).await?;
|
let messages = Chats::messages(&client, id, pagination).await?;
|
||||||
Ok(Json(messages))
|
Ok(Json(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_chat(client: &impl GenericClient, id: Uuid, user: Uuid) -> Result<(), Error> {
|
async fn check_chat(client: &impl GenericClient, id: Uuid, user: Uuid) -> Result<(), Error> {
|
||||||
match Chat::exists_and_has_access(client, id, user).await? {
|
match Chats::exists_and_has_access(client, id, user).await? {
|
||||||
Some(true) => Ok(()),
|
Some(true) => Ok(()),
|
||||||
Some(false) => Err(ApiError::FORBIDDEN.into()),
|
Some(false) => Err(ApiError::FORBIDDEN.into()),
|
||||||
None => Err(ApiError::NOT_FOUND.into()),
|
None => Err(ApiError::NOT_FOUND.into()),
|
||||||
@ -110,13 +94,13 @@ pub async fn send_message(
|
|||||||
) -> Result<Json<ChatMessage>, Error> {
|
) -> Result<Json<ChatMessage>, Error> {
|
||||||
let mut client = state.conn().await?;
|
let mut client = state.conn().await?;
|
||||||
check_chat(&client, id, auth.user_id()).await?;
|
check_chat(&client, id, auth.user_id()).await?;
|
||||||
let message = Chat::send(&mut client, id, auth.user_id(), body.text, body.extra).await?;
|
let message = Chats::send(&mut client, id, auth.user_id(), body.text, body.extra).await?;
|
||||||
let notfication = SocketMessage::Event(SocketEvent::MessageReceived {
|
let notfication = SocketMessage::Event(SocketEvent::MessageReceived {
|
||||||
chat: id,
|
chat: id,
|
||||||
from: auth.user_id(),
|
from: auth.user_id(),
|
||||||
message_id: message.id,
|
message_id: message.id,
|
||||||
});
|
});
|
||||||
for id in Chat::member_ids(&client, id).await? {
|
for id in Chats::member_ids(&client, id).await? {
|
||||||
if id == auth.user_id() {
|
if id == auth.user_id() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
310
src/api/mod.rs
310
src/api/mod.rs
@ -7,9 +7,9 @@ use axum::{
|
|||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bank_core::{ApiError, make_schemas, pagination::RequestPagination};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey};
|
use jsonwebtoken::{DecodingKey, EncodingKey};
|
||||||
use schemars::{JsonSchema, Schema};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
|
|
||||||
use tracing_error::SpanTrace;
|
use tracing_error::SpanTrace;
|
||||||
|
|
||||||
pub use axum::extract::State as EState;
|
pub use axum::extract::State as EState;
|
||||||
@ -30,7 +30,7 @@ pub struct Json<T>(T);
|
|||||||
macro_rules! rejection_error {
|
macro_rules! rejection_error {
|
||||||
($id:expr, $rejection:expr) => {{
|
($id:expr, $rejection:expr) => {{
|
||||||
let rejection = $rejection;
|
let rejection = $rejection;
|
||||||
$crate::api::ApiError {
|
bank_core::ApiError {
|
||||||
status: rejection.status(),
|
status: rejection.status(),
|
||||||
id: Cow::Borrowed($id),
|
id: Cow::Borrowed($id),
|
||||||
message: Cow::Owned(rejection.body_text()),
|
message: Cow::Owned(rejection.body_text()),
|
||||||
@ -82,6 +82,7 @@ impl<T: DeserializeOwned, S: Send + Sync> FromRequestParts<S> for Query<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
|
#[allow(unused)]
|
||||||
trace: Option<SpanTrace>,
|
trace: Option<SpanTrace>,
|
||||||
inner: InnerError,
|
inner: InnerError,
|
||||||
}
|
}
|
||||||
@ -95,58 +96,6 @@ pub enum InnerError {
|
|||||||
Validation(ValidationErrors),
|
Validation(ValidationErrors),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ApiError<'a> {
|
|
||||||
#[serde(skip)]
|
|
||||||
pub status: StatusCode,
|
|
||||||
pub id: Cow<'a, str>,
|
|
||||||
pub message: Cow<'a, str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ApiError<'a> {
|
|
||||||
pub const NOT_FOUND: ApiError<'static> =
|
|
||||||
ApiError::const_new(StatusCode::NOT_FOUND, "not-found", "Not found");
|
|
||||||
pub const INTERNAL_SERVER_ERROR: ApiError<'static> = ApiError::const_new(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"internal-server-error",
|
|
||||||
"Internal Server Error",
|
|
||||||
);
|
|
||||||
pub const FORBIDDEN: ApiError<'static> =
|
|
||||||
ApiError::const_new(StatusCode::FORBIDDEN, "forbidden", "Forbidden");
|
|
||||||
pub const fn const_new(status: StatusCode, id: &'static str, message: &'static str) -> Self {
|
|
||||||
Self {
|
|
||||||
status,
|
|
||||||
id: Cow::Borrowed(id),
|
|
||||||
message: Cow::Borrowed(message),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn new(
|
|
||||||
status: StatusCode,
|
|
||||||
id: impl Into<Cow<'a, str>>,
|
|
||||||
message: impl Into<Cow<'a, str>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
status,
|
|
||||||
id: id.into(),
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn into_static(self) -> ApiError<'static> {
|
|
||||||
ApiError {
|
|
||||||
status: self.status,
|
|
||||||
id: Cow::Owned(self.id.into_owned()),
|
|
||||||
message: Cow::Owned(self.message.into_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoResponse for ApiError<'a> {
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
(self.status, axum::Json(self)).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
pub fn new(inner: InnerError) -> Self {
|
pub fn new(inner: InnerError) -> Self {
|
||||||
let trace = if cfg!(debug_assertions) || inner.internal() {
|
let trace = if cfg!(debug_assertions) || inner.internal() {
|
||||||
@ -228,12 +177,13 @@ struct ValidationError {
|
|||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "schemas")]
|
||||||
impl schemars::JsonSchema for ValidationError {
|
impl schemars::JsonSchema for ValidationError {
|
||||||
fn schema_name() -> Cow<'static, str> {
|
fn schema_name() -> Cow<'static, str> {
|
||||||
Cow::Borrowed("SingleValidationError")
|
Cow::Borrowed("SingleValidationError")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> Schema {
|
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||||
schemars::Schema::try_from(serde_json::json!({
|
schemars::Schema::try_from(serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -336,253 +286,19 @@ pub fn router() -> Router<Arc<AppState>> {
|
|||||||
.nest("/socket", socket::router())
|
.nest("/socket", socket::router())
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((); (ApiError, _ValidationErrors), [crate::model::schemas, auth::schemas, user::schemas, transactions::schemas, account::schemas, chats::schemas]);
|
make_schemas!((); (ApiError, _ValidationErrors), [ bank_core::schemas, transactions::schemas]);
|
||||||
|
|
||||||
macro_rules! make_schemas {
|
#[derive(Deserialize)]
|
||||||
(($($request_bodies:ty),*); ($($response_bodies:ty),*)$(, [$($deps:expr),*])? ) => {
|
#[repr(transparent)]
|
||||||
#[cfg(feature = "schemas")]
|
#[serde(transparent)]
|
||||||
#[allow(unused)]
|
pub struct PaginationQuery(pub RequestPagination);
|
||||||
pub const fn schemas() -> $crate::api::Schemas {
|
|
||||||
fn request_bodies(generator: &mut schemars::SchemaGenerator, schemas: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {
|
|
||||||
$(
|
|
||||||
generator.subschema_for::<$request_bodies>();
|
|
||||||
// schemas.insert(<$request_bodies as schemars::JsonSchema>::schema_name(), generator.root_schema_for::<$request_bodies>());
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
fn response_bodies(generator: &mut schemars::SchemaGenerator, schemas: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {
|
|
||||||
$(
|
|
||||||
generator.subschema_for::<$response_bodies>();
|
|
||||||
// schemas.insert(<$response_bodies as schemars::JsonSchema>::schema_name(), generator.root_schema_for::<$response_bodies>());
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
static DEPS: &'static [$crate::api::Schemas] = &[$($($deps()),*)?];
|
|
||||||
$crate::api::Schemas {
|
|
||||||
request_bodies,
|
|
||||||
response_bodies,
|
|
||||||
contains: DEPS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "schemas"))]
|
|
||||||
pub const fn schemas() -> $crate::api::Schemas {
|
|
||||||
$crate::api::Schemas
|
|
||||||
}
|
|
||||||
};
|
|
||||||
([$($deps:expr),*]) => {
|
|
||||||
#[cfg(feature = "schemas")]
|
|
||||||
pub const fn schemas() -> $crate::api::Schemas {
|
|
||||||
fn ident(_: &mut schemars::SchemaGenerator, _: &mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>) {}
|
|
||||||
static DEPS: &'static [$crate::api::Schemas] = &[$($deps()),*];
|
|
||||||
$crate::api::Schemas {
|
|
||||||
request_bodies: ident,
|
|
||||||
response_bodies: ident,
|
|
||||||
contains: DEPS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "schemas"))]
|
|
||||||
pub const fn schemas() -> $crate::api::Schemas {
|
|
||||||
$crate::api::Schemas
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) use make_schemas;
|
impl<S: Send + Sync> FromRequestParts<S> for PaginationQuery {
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct RequestPagination {
|
|
||||||
#[serde(deserialize_with = "deserialize_u64")]
|
|
||||||
pub limit: u64,
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde(deserialize_with = "deserialize_u64")]
|
|
||||||
pub offset: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! visit {
|
|
||||||
(signed $ty:ident : $visit:ident) => {
|
|
||||||
#[inline]
|
|
||||||
fn $visit<E>(self, v: $ty) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
if 0 <= v && v as u64 <= Self::Value::max_value() as u64 {
|
|
||||||
Ok(v as Self::Value)
|
|
||||||
} else {
|
|
||||||
Err(E::invalid_value(
|
|
||||||
serde::de::Unexpected::Signed(v as i64),
|
|
||||||
&self,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($ty:ident : $visit:ident) => {
|
|
||||||
#[inline]
|
|
||||||
fn $visit<E>(self, v: $ty) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
if v as u64 <= Self::Value::max_value() as u64 {
|
|
||||||
Ok(v as Self::Value)
|
|
||||||
} else {
|
|
||||||
Err(E::invalid_value(
|
|
||||||
serde::de::Unexpected::Unsigned(v as u64),
|
|
||||||
&self,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct Helper;
|
|
||||||
impl<'a> serde::de::Visitor<'a> for Helper {
|
|
||||||
type Value = u64;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("a u64")
|
|
||||||
}
|
|
||||||
|
|
||||||
visit!(signed i8: visit_i8);
|
|
||||||
visit!(signed i16: visit_i16);
|
|
||||||
visit!(signed i32: visit_i32);
|
|
||||||
visit!(signed i64: visit_i64);
|
|
||||||
|
|
||||||
visit!(u8: visit_u8);
|
|
||||||
visit!(u16: visit_u16);
|
|
||||||
visit!(u32: visit_u32);
|
|
||||||
visit!(u64: visit_u64);
|
|
||||||
|
|
||||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
match v.parse() {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(_) => Err(E::invalid_value(
|
|
||||||
serde::de::Unexpected::Str(v),
|
|
||||||
&"valid unsigned integer",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deserializer.deserialize_any(Helper)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: Send + Sync> FromRequestParts<S> for RequestPagination {
|
|
||||||
type Rejection = ApiError<'static>;
|
type Rejection = ApiError<'static>;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
Query::<RequestPagination>::from_request_parts(parts, state)
|
Query::<RequestPagination>::from_request_parts(parts, state)
|
||||||
.await
|
.await
|
||||||
.map(|query| query.0)
|
.map(|query| Self(query.0))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequestPagination {
|
|
||||||
#[inline]
|
|
||||||
pub const fn limit(&self) -> i64 {
|
|
||||||
self.limit as i64
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub const fn offset(&self) -> i64 {
|
|
||||||
self.offset as i64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
#[serde(rename = "Pagination")]
|
|
||||||
pub struct PaginationMeta {
|
|
||||||
pub total: u64,
|
|
||||||
pub limit: u64,
|
|
||||||
pub offset: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
// #[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct Pagination<T> {
|
|
||||||
pub pagination: PaginationMeta,
|
|
||||||
pub result: Vec<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Pagination<T> {
|
|
||||||
pub fn new(result: Vec<T>, total: u64, pagination: RequestPagination) -> Self {
|
|
||||||
Self {
|
|
||||||
pagination: PaginationMeta {
|
|
||||||
total,
|
|
||||||
limit: pagination.limit,
|
|
||||||
offset: pagination.offset,
|
|
||||||
},
|
|
||||||
result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PaginationType {
|
|
||||||
const NAME: &str;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: PaginationType + JsonSchema> JsonSchema for Pagination<T> {
|
|
||||||
fn schema_name() -> Cow<'static, str> {
|
|
||||||
format!("Paginated{}", T::NAME).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema_id() -> Cow<'static, str> {
|
|
||||||
Cow::Borrowed(std::any::type_name::<Self>())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
|
||||||
let result_schema = generator.subschema_for::<Vec<T>>();
|
|
||||||
let pagination_schema = generator.subschema_for::<PaginationMeta>();
|
|
||||||
schemars::Schema::try_from(serde_json::json!({
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"pagination": pagination_schema,
|
|
||||||
"result": result_schema
|
|
||||||
},
|
|
||||||
"required": ["pagination", "result"]
|
|
||||||
}))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "schemas"))]
|
|
||||||
pub struct Schemas;
|
|
||||||
#[cfg(feature = "schemas")]
|
|
||||||
pub struct Schemas {
|
|
||||||
pub request_bodies: fn(
|
|
||||||
&mut schemars::SchemaGenerator,
|
|
||||||
&mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>,
|
|
||||||
),
|
|
||||||
pub response_bodies: fn(
|
|
||||||
&mut schemars::SchemaGenerator,
|
|
||||||
&mut std::collections::HashMap<std::borrow::Cow<'static, str>, schemars::Schema>,
|
|
||||||
),
|
|
||||||
pub contains: &'static [Self],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "schemas")]
|
|
||||||
impl Schemas {
|
|
||||||
pub fn generate(
|
|
||||||
&self,
|
|
||||||
requests: &mut schemars::SchemaGenerator,
|
|
||||||
responses: &mut schemars::SchemaGenerator,
|
|
||||||
request_schemas: &mut std::collections::HashMap<
|
|
||||||
std::borrow::Cow<'static, str>,
|
|
||||||
schemars::Schema,
|
|
||||||
>,
|
|
||||||
response_schemas: &mut std::collections::HashMap<
|
|
||||||
std::borrow::Cow<'static, str>,
|
|
||||||
schemars::Schema,
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
for schemas in self.contains {
|
|
||||||
schemas.generate(requests, responses, request_schemas, response_schemas);
|
|
||||||
}
|
|
||||||
(self.request_bodies)(requests, request_schemas);
|
|
||||||
(self.response_bodies)(requests, response_schemas);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,14 +18,12 @@ use serde::Serialize;
|
|||||||
use tracing::{error, info, instrument};
|
use tracing::{error, info, instrument};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{AppState, EState, Error, State, auth::Auth, make_schemas};
|
use super::{AppState, EState, Error, State, auth::Auth};
|
||||||
|
|
||||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new().route("/", get(handle))
|
Router::new().route("/", get(handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((); ());
|
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn handle(
|
pub async fn handle(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
|
|||||||
@ -1,21 +1,23 @@
|
|||||||
use std::{borrow::Cow, cell::RefCell, sync::Arc};
|
use std::{borrow::Cow, cell::RefCell, sync::Arc};
|
||||||
|
|
||||||
use axum::{Router, routing::post};
|
use axum::{Router, routing::post};
|
||||||
|
use bank_core::{
|
||||||
|
Name, NameOrUuid, USER_ACCOUNT_PATTERN, make_schemas,
|
||||||
|
pagination::Pagination,
|
||||||
|
transaction::{FullTransaction, Transaction, TransactionQuery},
|
||||||
|
};
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use garde::Validate;
|
use garde::Validate;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{Accounts, Transactions, Users};
|
||||||
Account, Direction, FullTransaction, Name, Transaction, USER_ACCOUNT_PATTERN, User,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppState, EState, Error, Json, Pagination, PaginationType, Query, RequestPagination, State,
|
AppState, EState, Error, Json, Query, State,
|
||||||
auth::Auth,
|
auth::Auth,
|
||||||
make_schemas,
|
|
||||||
socket::{SocketEvent, SocketMessage},
|
socket::{SocketEvent, SocketMessage},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,17 +25,7 @@ pub(super) fn router() -> Router<Arc<AppState>> {
|
|||||||
Router::new().route("/", post(make_payment))
|
Router::new().route("/", post(make_payment))
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((MakePayment); (Pagination<FullTransaction>));
|
make_schemas!((MakePayment); ());
|
||||||
|
|
||||||
impl PaginationType for FullTransaction {
|
|
||||||
const NAME: &str = "Transactions";
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct TransactionHistory {
|
|
||||||
transactions: Vec<Transaction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
#[serde(untagged, rename = "PaymentTarget")]
|
#[serde(untagged, rename = "PaymentTarget")]
|
||||||
@ -122,26 +114,6 @@ pub struct BodyMakePayment {
|
|||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Validate, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum NameOrUuid {
|
|
||||||
Id(#[garde(skip)] Uuid),
|
|
||||||
Name(#[garde(dive)] Name),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NameOrUuid {
|
|
||||||
pub async fn user_id(
|
|
||||||
&self,
|
|
||||||
client: &impl GenericClient,
|
|
||||||
) -> Result<Option<Uuid>, tokio_postgres::Error> {
|
|
||||||
match self {
|
|
||||||
NameOrUuid::Id(id) => Ok(User::exists(client, *id).await?.then_some(*id)),
|
|
||||||
NameOrUuid::Name(name) => Ok(User::info_by_name(client, &name.0).await?.map(|v| v.id)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Validate, PartialEq)]
|
#[derive(Debug, Validate, PartialEq)]
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
||||||
pub struct AccountSelector {
|
pub struct AccountSelector {
|
||||||
@ -159,13 +131,13 @@ impl AccountSelector {
|
|||||||
) -> Result<Option<(Uuid, Uuid)>, tokio_postgres::Error> {
|
) -> Result<Option<(Uuid, Uuid)>, tokio_postgres::Error> {
|
||||||
let user_id = match &self.user {
|
let user_id = match &self.user {
|
||||||
NameOrUuid::Id(uuid) => *uuid,
|
NameOrUuid::Id(uuid) => *uuid,
|
||||||
NameOrUuid::Name(name) => match User::info_by_name(client, &*name).await? {
|
NameOrUuid::Name(name) => match Users::info_by_name(client, &*name).await? {
|
||||||
Some(info) => info.id,
|
Some(info) => info.id,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let account_id = match self.account.as_ref() {
|
let account_id = match self.account.as_ref() {
|
||||||
Some(name) => match Account::get_for_user(client, user_id, &*name).await? {
|
Some(name) => match Accounts::get_for_user(client, user_id, &*name).await? {
|
||||||
Some(info) => info.id,
|
Some(info) => info.id,
|
||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
},
|
},
|
||||||
@ -350,13 +322,6 @@ impl MakePayment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct TransactionQuery {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub pagination: RequestPagination,
|
|
||||||
pub direction: Option<Direction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn transaction_history(
|
pub async fn transaction_history(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
@ -366,7 +331,7 @@ pub async fn transaction_history(
|
|||||||
}): Query<TransactionQuery>,
|
}): Query<TransactionQuery>,
|
||||||
) -> Result<Json<Pagination<FullTransaction>>, Error> {
|
) -> Result<Json<Pagination<FullTransaction>>, Error> {
|
||||||
let client = state.conn().await?;
|
let client = state.conn().await?;
|
||||||
let result = Transaction::user_history(&client, auth.user_id(), direction, pagination).await?;
|
let result = Transactions::user_history(&client, auth.user_id(), direction, pagination).await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
pub async fn make_payment(
|
pub async fn make_payment(
|
||||||
@ -381,8 +346,8 @@ pub async fn make_payment(
|
|||||||
let mut client = client.transaction().await?;
|
let mut client = client.transaction().await?;
|
||||||
|
|
||||||
let Some(from) = (match from {
|
let Some(from) = (match from {
|
||||||
NameOrUuid::Id(uuid) => Account::by_id(&client, uuid).await?,
|
NameOrUuid::Id(uuid) => Accounts::by_id(&client, uuid).await?,
|
||||||
NameOrUuid::Name(name) => Account::get_for_user(&client, user_id, &*name).await?,
|
NameOrUuid::Name(name) => Accounts::get_for_user(&client, user_id, &*name).await?,
|
||||||
}) else {
|
}) else {
|
||||||
todo!("from account doesn't exist")
|
todo!("from account doesn't exist")
|
||||||
};
|
};
|
||||||
@ -401,7 +366,7 @@ pub async fn make_payment(
|
|||||||
client
|
client
|
||||||
.execute(&update_balance_stmt, &[&to, &(amount as i64)])
|
.execute(&update_balance_stmt, &[&to, &(amount as i64)])
|
||||||
.await?;
|
.await?;
|
||||||
let transaction = Transaction::create(&mut client, from.id, to, amount, None).await?;
|
let transaction = Transactions::create(&mut client, from.id, to, amount, None).await?;
|
||||||
client.commit().await?;
|
client.commit().await?;
|
||||||
state
|
state
|
||||||
.sockets
|
.sockets
|
||||||
@ -419,11 +384,10 @@ pub async fn make_payment(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use bank_core::Name;
|
||||||
api::transactions::{
|
|
||||||
|
use crate::api::transactions::{
|
||||||
AccountSelector, NameOrUuid, UnvalidatedAccountSelector, UnvalidatedTransform,
|
AccountSelector, NameOrUuid, UnvalidatedAccountSelector, UnvalidatedTransform,
|
||||||
},
|
|
||||||
model::Name,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::MakePayment;
|
use super::MakePayment;
|
||||||
|
|||||||
@ -1,19 +1,21 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{Router, extract::Path, routing::get};
|
use axum::{Router, extract::Path, routing::get};
|
||||||
|
use bank_core::{
|
||||||
|
pagination::Pagination,
|
||||||
|
transaction::{FullTransaction, TransactionQuery},
|
||||||
|
user::{User, UserAccounts, UserBalance},
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::ApiError,
|
api::ApiError,
|
||||||
model::{Account, FullTransaction, Transaction, User, UserAccountInfo},
|
model::{Accounts, Transactions, Users},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{AppState, EState, Error, Json, PaginationQuery, Query, State, auth::Auth};
|
||||||
AppState, EState, Error, Json, Pagination, Query, RequestPagination, State, auth::Auth,
|
|
||||||
make_schemas, transactions::TransactionQuery,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
@ -40,20 +42,6 @@ impl UserTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((); (Pagination<User>, UserAccounts, UserBalance));
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct UserAccounts {
|
|
||||||
pub result: Vec<UserAccountInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct UserBalance {
|
|
||||||
pub balance: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn user_info(
|
pub async fn user_info(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
@ -61,7 +49,7 @@ pub async fn user_info(
|
|||||||
) -> Result<Json<User>, Error> {
|
) -> Result<Json<User>, Error> {
|
||||||
let user = target.user_id(&auth);
|
let user = target.user_id(&auth);
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let info = User::info(&conn, user).await?;
|
let info = Users::info(&conn, user).await?;
|
||||||
if let Some(info) = info {
|
if let Some(info) = info {
|
||||||
return Ok(Json(info));
|
return Ok(Json(info));
|
||||||
}
|
}
|
||||||
@ -74,7 +62,7 @@ pub async fn user_info(
|
|||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn user_balance(EState(state): State, auth: Auth) -> Result<Json<UserBalance>, Error> {
|
pub async fn user_balance(EState(state): State, auth: Auth) -> Result<Json<UserBalance>, Error> {
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let info = Account::list_for_user(&conn, auth.user_id()).await?;
|
let info = Accounts::list_for_user(&conn, auth.user_id()).await?;
|
||||||
let balance = info.iter().map(|info| info.balance).sum();
|
let balance = info.iter().map(|info| info.balance).sum();
|
||||||
Ok(Json(UserBalance { balance }))
|
Ok(Json(UserBalance { balance }))
|
||||||
}
|
}
|
||||||
@ -83,10 +71,10 @@ pub async fn user_balance(EState(state): State, auth: Auth) -> Result<Json<UserB
|
|||||||
pub async fn list_users(
|
pub async fn list_users(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
_: Auth,
|
_: Auth,
|
||||||
pagination: RequestPagination,
|
PaginationQuery(pagination): PaginationQuery,
|
||||||
) -> Result<Json<Pagination<User>>, Error> {
|
) -> Result<Json<Pagination<User>>, Error> {
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let users = User::list(&conn, pagination).await?;
|
let users = Users::list(&conn, pagination).await?;
|
||||||
Ok(Json(users))
|
Ok(Json(users))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +88,7 @@ pub async fn me_transaction_history(
|
|||||||
}): Query<TransactionQuery>,
|
}): Query<TransactionQuery>,
|
||||||
) -> Result<Json<Pagination<FullTransaction>>, Error> {
|
) -> Result<Json<Pagination<FullTransaction>>, Error> {
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let result = Transaction::user_history(&conn, auth.user_id(), direction, pagination).await?;
|
let result = Transactions::user_history(&conn, auth.user_id(), direction, pagination).await?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +96,7 @@ pub async fn me_transaction_history(
|
|||||||
pub async fn user_accounts(EState(state): State, auth: Auth) -> Result<Json<UserAccounts>, Error> {
|
pub async fn user_accounts(EState(state): State, auth: Auth) -> Result<Json<UserAccounts>, Error> {
|
||||||
let user = auth.user_id();
|
let user = auth.user_id();
|
||||||
let conn = state.conn().await?;
|
let conn = state.conn().await?;
|
||||||
let result = Account::list_for_user(&conn, user).await?;
|
let result = Accounts::list_for_user(&conn, user).await?;
|
||||||
Ok(Json(UserAccounts { result }))
|
Ok(Json(UserAccounts { result }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,72 +1,32 @@
|
|||||||
|
use bank_core::{
|
||||||
|
account::{AccountInfo, UserAccountInfo},
|
||||||
|
pagination::{Pagination, RequestPagination},
|
||||||
|
};
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use serde::Serialize;
|
|
||||||
use tokio_postgres::Row;
|
use tokio_postgres::Row;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::{NoContext, Timestamp, Uuid};
|
use uuid::{NoContext, Timestamp, Uuid};
|
||||||
|
|
||||||
use crate::api::{Pagination, PaginationType, RequestPagination};
|
|
||||||
|
|
||||||
use super::count;
|
use super::count;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
fn account_info_from_row(row: Row) -> AccountInfo {
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
AccountInfo {
|
||||||
pub struct Account {
|
id: row.get("id"),
|
||||||
pub id: Uuid,
|
user: row.get("user"),
|
||||||
pub user: Uuid,
|
name: row.get("name"),
|
||||||
pub name: String,
|
|
||||||
pub balance: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct AccountInfo {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub user: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct UserAccountInfo {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
pub balance: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ReducedAccountInfo {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginationType for UserAccountInfo {
|
|
||||||
const NAME: &str = "UserAccounts";
|
|
||||||
}
|
|
||||||
impl PaginationType for AccountInfo {
|
|
||||||
const NAME: &str = "Accounts";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Row> for AccountInfo {
|
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.get("id"),
|
|
||||||
user: value.get("user"),
|
|
||||||
name: value.get("name"),
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
fn user_account_info_from_row(row: Row) -> UserAccountInfo {
|
||||||
|
UserAccountInfo {
|
||||||
|
id: row.get("id"),
|
||||||
|
name: row.get("name"),
|
||||||
|
balance: row.get::<_, i64>("balance") as u64,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Row> for UserAccountInfo {
|
pub struct Accounts;
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.get("id"),
|
|
||||||
name: value.get("name"),
|
|
||||||
balance: value.get::<_, i64>("balance") as u64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Account {
|
impl Accounts {
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
client: &mut impl GenericClient,
|
client: &mut impl GenericClient,
|
||||||
@ -102,7 +62,7 @@ impl Account {
|
|||||||
.query(&stmt, &[&pagination.limit(), &pagination.offset()])
|
.query(&stmt, &[&pagination.limit(), &pagination.offset()])
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(AccountInfo::from)
|
.map(account_info_from_row)
|
||||||
.collect();
|
.collect();
|
||||||
let total = count(client, &stmt_count, &[]).await?;
|
let total = count(client, &stmt_count, &[]).await?;
|
||||||
Ok(Pagination::new(users, total, pagination))
|
Ok(Pagination::new(users, total, pagination))
|
||||||
@ -120,7 +80,7 @@ impl Account {
|
|||||||
.query(&stmt, &[&user])
|
.query(&stmt, &[&user])
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(UserAccountInfo::from)
|
.map(user_account_info_from_row)
|
||||||
.collect();
|
.collect();
|
||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
@ -133,7 +93,7 @@ impl Account {
|
|||||||
.prepare_cached("select id,name,balance from accounts where id =$1")
|
.prepare_cached("select id,name,balance from accounts where id =$1")
|
||||||
.await?;
|
.await?;
|
||||||
let res = client.query_opt(&stmt, &[&id]).await?;
|
let res = client.query_opt(&stmt, &[&id]).await?;
|
||||||
Ok(res.map(UserAccountInfo::from))
|
Ok(res.map(user_account_info_from_row))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn owned_by(
|
pub async fn owned_by(
|
||||||
@ -161,6 +121,6 @@ impl Account {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let res = client.query_opt(&stmt, &[&user, &name]).await?;
|
let res = client.query_opt(&stmt, &[&user, &name]).await?;
|
||||||
Ok(res.map(UserAccountInfo::from))
|
Ok(res.map(user_account_info_from_row))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +1,42 @@
|
|||||||
|
use bank_core::{
|
||||||
|
chat::{Chat, ChatInfo, ChatMessage},
|
||||||
|
pagination::{Pagination, RequestPagination},
|
||||||
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use serde::Serialize;
|
|
||||||
use tokio_postgres::Row;
|
use tokio_postgres::Row;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::{NoContext, Timestamp, Uuid};
|
use uuid::{NoContext, Timestamp, Uuid};
|
||||||
|
|
||||||
use crate::api::{Pagination, PaginationType, RequestPagination};
|
|
||||||
|
|
||||||
use super::count;
|
use super::count;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
fn chat_from_row(row: Row) -> Chat {
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct Chat {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ChatInfo {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
pub read_until: Option<Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct ChatMessage {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub sender: Uuid,
|
|
||||||
pub time: DateTime<Utc>,
|
|
||||||
pub text: String,
|
|
||||||
pub extra: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginationType for Chat {
|
|
||||||
const NAME: &str = "Chats";
|
|
||||||
}
|
|
||||||
impl PaginationType for ChatInfo {
|
|
||||||
const NAME: &str = "ChatInfos";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaginationType for ChatMessage {
|
|
||||||
const NAME: &str = "ChatMessages";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Row> for Chat {
|
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.get("id"),
|
|
||||||
created: value.get("created"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<Row> for ChatInfo {
|
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.get("id"),
|
|
||||||
created: value.get("created"),
|
|
||||||
read_until: value.get("read_until"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ChatInfo> for Chat {
|
|
||||||
fn from(value: ChatInfo) -> Self {
|
|
||||||
Chat {
|
Chat {
|
||||||
id: value.id,
|
id: row.get("id"),
|
||||||
created: value.created,
|
created: row.get("created"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
fn chat_info_from_row(row: Row) -> ChatInfo {
|
||||||
|
ChatInfo {
|
||||||
|
id: row.get("id"),
|
||||||
|
created: row.get("created"),
|
||||||
|
read_until: row.get("read_until"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Row> for ChatMessage {
|
fn chat_message_from_row(row: Row) -> ChatMessage {
|
||||||
fn from(value: Row) -> Self {
|
ChatMessage {
|
||||||
Self {
|
id: row.get("id"),
|
||||||
id: value.get("id"),
|
sender: row.get("sender"),
|
||||||
sender: value.get("sender"),
|
time: row.get("timestamp"),
|
||||||
time: value.get("timestamp"),
|
text: row.get("text"),
|
||||||
text: value.get("text"),
|
extra: row.get("extra"),
|
||||||
extra: value.get("extra"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
pub struct Chats;
|
||||||
|
|
||||||
|
impl Chats {
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
pub async fn exists(
|
pub async fn exists(
|
||||||
client: &impl GenericClient,
|
client: &impl GenericClient,
|
||||||
@ -120,7 +73,7 @@ impl Chat {
|
|||||||
.prepare_cached("select * from chats where id = $1")
|
.prepare_cached("select * from chats where id = $1")
|
||||||
.await?;
|
.await?;
|
||||||
let res = client.query_opt(&stmt, &[&id]).await?;
|
let res = client.query_opt(&stmt, &[&id]).await?;
|
||||||
Ok(res.map(Chat::from))
|
Ok(res.map(chat_from_row))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
@ -131,7 +84,7 @@ impl Chat {
|
|||||||
) -> Result<Option<ChatInfo>, tokio_postgres::Error> {
|
) -> Result<Option<ChatInfo>, tokio_postgres::Error> {
|
||||||
let stmt = client.prepare_cached("select c.id as id, c.created as created, cm.read_until as read_until from chat_members cm join chats c on cm.chat = c.id where c.id = $1 and cm.\"user\" = $2").await?;
|
let stmt = client.prepare_cached("select c.id as id, c.created as created, cm.read_until as read_until from chat_members cm join chats c on cm.chat = c.id where c.id = $1 and cm.\"user\" = $2").await?;
|
||||||
let res = client.query_opt(&stmt, &[&id, &user]).await?;
|
let res = client.query_opt(&stmt, &[&id, &user]).await?;
|
||||||
Ok(res.map(ChatInfo::from))
|
Ok(res.map(chat_info_from_row))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn member_ids(
|
pub async fn member_ids(
|
||||||
@ -181,7 +134,7 @@ impl Chat {
|
|||||||
client.execute(&insert_member, &[&id, &target]).await?;
|
client.execute(&insert_member, &[&id, &target]).await?;
|
||||||
|
|
||||||
client.commit().await?;
|
client.commit().await?;
|
||||||
Ok(Self {
|
Ok(Chat {
|
||||||
id,
|
id,
|
||||||
created: result.get(0),
|
created: result.get(0),
|
||||||
})
|
})
|
||||||
@ -199,7 +152,7 @@ impl Chat {
|
|||||||
.query(&stmt, &[&user, &pagination.limit(), &pagination.offset()])
|
.query(&stmt, &[&user, &pagination.limit(), &pagination.offset()])
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Chat::from)
|
.map(chat_from_row)
|
||||||
.collect();
|
.collect();
|
||||||
let total = count(client, &count_stmt, &[&user]).await?;
|
let total = count(client, &count_stmt, &[&user]).await?;
|
||||||
Ok(Pagination::new(result, total, pagination))
|
Ok(Pagination::new(result, total, pagination))
|
||||||
@ -220,7 +173,7 @@ impl Chat {
|
|||||||
.query(&stmt, &[&chat, &pagination.limit(), &pagination.offset()])
|
.query(&stmt, &[&chat, &pagination.limit(), &pagination.offset()])
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ChatMessage::from)
|
.map(chat_message_from_row)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let total = count(client, &count_stmt, &[&chat]).await?;
|
let total = count(client, &count_stmt, &[&chat]).await?;
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
use std::{ops::Deref, sync::LazyLock};
|
use bank_core::NameOrUuid;
|
||||||
|
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use garde::Validate;
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio_postgres::{ToStatement, types::ToSql};
|
use tokio_postgres::{ToStatement, types::ToSql};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -13,44 +9,28 @@ mod chats;
|
|||||||
mod transaction;
|
mod transaction;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
pub use account::{Account, AccountInfo, UserAccountInfo};
|
pub use account::Accounts;
|
||||||
pub use chats::{Chat, ChatInfo, ChatMessage};
|
pub use chats::Chats;
|
||||||
pub use transaction::{Direction, FullTransaction, Transaction};
|
pub use transaction::Transactions;
|
||||||
pub use user::User;
|
pub use user::Users;
|
||||||
|
|
||||||
use crate::api::make_schemas;
|
pub(crate) trait NameOrUuidExt {
|
||||||
|
async fn user_id(
|
||||||
static NAME_PATTERN_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(NAME_PATTERN).unwrap());
|
&self,
|
||||||
|
client: &impl GenericClient,
|
||||||
pub const NAME_PATTERN: &str = "^[a-z0-9_-]+$";
|
) -> Result<Option<Uuid>, tokio_postgres::Error>;
|
||||||
pub const USER_ACCOUNT_PATTERN: &str = "([a-z0-9_-]+):([a-z0-9_-]+)";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Validate, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
#[garde(transparent)]
|
|
||||||
pub struct Name(#[garde(length(min = 3, max = 32), pattern(NAME_PATTERN_RE))] pub String);
|
|
||||||
|
|
||||||
impl Deref for Name {
|
|
||||||
type Target = str;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Name {
|
|
||||||
pub fn into_inner(self) -> String {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_name(name: &str) -> Result<(), garde::Error> {
|
|
||||||
garde::rules::pattern::apply(&name, (&NAME_PATTERN_RE,))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl NameOrUuidExt for NameOrUuid {
|
||||||
pub struct IdWithName {
|
async fn user_id(
|
||||||
pub id: Uuid,
|
&self,
|
||||||
pub name: Name,
|
client: &impl GenericClient,
|
||||||
|
) -> Result<Option<Uuid>, tokio_postgres::Error> {
|
||||||
|
match self {
|
||||||
|
NameOrUuid::Id(id) => Ok(Users::exists(client, *id).await?.then_some(*id)),
|
||||||
|
NameOrUuid::Name(name) => Ok(Users::info_by_name(client, &name.0).await?.map(|v| v.id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
@ -61,5 +41,3 @@ async fn count<T: ?Sized + ToStatement + Sync + Send + std::fmt::Debug>(
|
|||||||
) -> Result<u64, tokio_postgres::Error> {
|
) -> Result<u64, tokio_postgres::Error> {
|
||||||
Ok(client.query_one(statement, params).await?.get::<_, i64>(0) as u64)
|
Ok(client.query_one(statement, params).await?.get::<_, i64>(0) as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
make_schemas!((Direction); (User, Account, AccountInfo, UserAccountInfo, Transaction, FullTransaction, Chat, ChatInfo, ChatMessage));
|
|
||||||
|
|||||||
@ -1,94 +1,46 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use bank_core::{
|
||||||
|
account::ReducedAccountInfo,
|
||||||
|
pagination::{Pagination, RequestPagination},
|
||||||
|
transaction::{Direction, FullTransaction, Participant, Transaction},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio_postgres::Row;
|
use tokio_postgres::Row;
|
||||||
use uuid::{NoContext, Timestamp, Uuid};
|
use uuid::{NoContext, Timestamp, Uuid};
|
||||||
|
|
||||||
use crate::{
|
use crate::model::count;
|
||||||
api::{Pagination, RequestPagination},
|
|
||||||
model::count,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{User, account::ReducedAccountInfo};
|
fn full_transaction_from_row(row: Row) -> FullTransaction {
|
||||||
|
FullTransaction {
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum Direction {
|
|
||||||
Received,
|
|
||||||
Sent,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct Participant {
|
|
||||||
user: User,
|
|
||||||
account: ReducedAccountInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct FullTransaction {
|
|
||||||
from: Participant,
|
|
||||||
to: Participant,
|
|
||||||
amount: u64,
|
|
||||||
timestamp: DateTime<Utc>,
|
|
||||||
message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Row> for FullTransaction {
|
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
from: Participant {
|
from: Participant {
|
||||||
user: User {
|
user: User {
|
||||||
id: value.get("from_user_id"),
|
id: row.get("from_user_id"),
|
||||||
name: value.get("from_user_name"),
|
name: row.get("from_user_name"),
|
||||||
},
|
},
|
||||||
account: ReducedAccountInfo {
|
account: ReducedAccountInfo {
|
||||||
id: value.get("from_account_id"),
|
id: row.get("from_account_id"),
|
||||||
name: value.get("from_account_name"),
|
name: row.get("from_account_name"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
to: Participant {
|
to: Participant {
|
||||||
user: User {
|
user: User {
|
||||||
id: value.get("to_user_id"),
|
id: row.get("to_user_id"),
|
||||||
name: value.get("to_user_name"),
|
name: row.get("to_user_name"),
|
||||||
},
|
},
|
||||||
account: ReducedAccountInfo {
|
account: ReducedAccountInfo {
|
||||||
id: value.get("to_account_id"),
|
id: row.get("to_account_id"),
|
||||||
name: value.get("to_account_name"),
|
name: row.get("to_account_name"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
timestamp: value.get("timestamp"),
|
timestamp: row.get("timestamp"),
|
||||||
amount: value.get::<_, i64>("amount") as u64,
|
amount: row.get::<_, i64>("amount") as u64,
|
||||||
message: value.get("message"),
|
message: row.get("message"),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
pub struct Transactions;
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct Transaction {
|
|
||||||
pub from: Uuid,
|
|
||||||
pub to: Uuid,
|
|
||||||
pub amount: u64,
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
pub message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Row> for Transaction {
|
impl Transactions {
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
from: value.get("from"),
|
|
||||||
to: value.get("to"),
|
|
||||||
amount: value.get::<_, i64>("amount") as u64,
|
|
||||||
timestamp: value.get("timestamp"),
|
|
||||||
message: value.get("message"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Transaction {
|
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
client: &mut impl GenericClient,
|
client: &mut impl GenericClient,
|
||||||
from: Uuid,
|
from: Uuid,
|
||||||
@ -104,7 +56,7 @@ impl Transaction {
|
|||||||
.query_one(&stmt, &[&id, &from, &to, &(amount as i64), &message])
|
.query_one(&stmt, &[&id, &from, &to, &(amount as i64), &message])
|
||||||
.await?;
|
.await?;
|
||||||
let timestamp = row.get(0);
|
let timestamp = row.get(0);
|
||||||
Ok(Self {
|
Ok(Transaction {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
amount,
|
amount,
|
||||||
@ -113,75 +65,6 @@ impl Transaction {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_to_account(
|
|
||||||
client: &mut impl GenericClient,
|
|
||||||
id: Uuid,
|
|
||||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
|
||||||
let stmt = client
|
|
||||||
.prepare_cached("select * from transactions where \"to\" = $1")
|
|
||||||
.await?;
|
|
||||||
let transactions = client
|
|
||||||
.query(&stmt, &[&id])
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(Transaction::from)
|
|
||||||
.collect();
|
|
||||||
Ok(transactions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_from_account(
|
|
||||||
client: &mut impl GenericClient,
|
|
||||||
id: Uuid,
|
|
||||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
|
||||||
let stmt = client
|
|
||||||
.prepare_cached("select * from transactions where \"from\" = $1")
|
|
||||||
.await?;
|
|
||||||
let transactions = client
|
|
||||||
.query(&stmt, &[&id])
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(Transaction::from)
|
|
||||||
.collect();
|
|
||||||
Ok(transactions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_for_account(
|
|
||||||
client: &mut impl GenericClient,
|
|
||||||
id: Uuid,
|
|
||||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
|
||||||
let stmt = client
|
|
||||||
.prepare_cached("select * from transactions where \"from\" = $1 or \"to\" = $1")
|
|
||||||
.await?;
|
|
||||||
let transactions = client
|
|
||||||
.query(&stmt, &[&id])
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(Transaction::from)
|
|
||||||
.collect();
|
|
||||||
Ok(transactions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_to_user(
|
|
||||||
_client: &mut impl GenericClient,
|
|
||||||
_id: Uuid,
|
|
||||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_from_user(
|
|
||||||
_client: &mut impl GenericClient,
|
|
||||||
_id: Uuid,
|
|
||||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_for_user(
|
|
||||||
_client: &mut impl GenericClient,
|
|
||||||
_id: Uuid,
|
|
||||||
) -> Result<Vec<Transaction>, tokio_postgres::Error> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn account_history(
|
pub async fn account_history(
|
||||||
client: &impl GenericClient,
|
client: &impl GenericClient,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@ -242,7 +125,7 @@ impl Transaction {
|
|||||||
.await?;
|
.await?;
|
||||||
let transactions = transactions
|
let transactions = transactions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(FullTransaction::from)
|
.map(full_transaction_from_row)
|
||||||
.collect();
|
.collect();
|
||||||
let total = count(client, &count_stmt, &[&id]).await?;
|
let total = count(client, &count_stmt, &[&id]).await?;
|
||||||
|
|
||||||
|
|||||||
@ -1,32 +1,24 @@
|
|||||||
use axum::http::StatusCode;
|
use bank_core::{
|
||||||
|
ApiError,
|
||||||
|
pagination::{Pagination, RequestPagination},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
use deadpool_postgres::GenericClient;
|
use deadpool_postgres::GenericClient;
|
||||||
use serde::Serialize;
|
|
||||||
use tokio_postgres::{Row, error::SqlState};
|
use tokio_postgres::{Row, error::SqlState};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::{NoContext, Timestamp, Uuid};
|
use uuid::{NoContext, Timestamp, Uuid};
|
||||||
|
|
||||||
use crate::api::{ApiError, Error, Pagination, PaginationType, RequestPagination};
|
use crate::api::Error;
|
||||||
|
|
||||||
use super::{Account, count};
|
use super::{Accounts, count};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
pub struct Users;
|
||||||
#[cfg_attr(feature = "schemas", derive(schemars::JsonSchema))]
|
|
||||||
pub struct User {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Row> for User {
|
pub fn user_from_row(row: Row) -> User {
|
||||||
fn from(value: Row) -> Self {
|
User {
|
||||||
Self {
|
id: row.get("id"),
|
||||||
id: value.get("id"),
|
name: row.get("name"),
|
||||||
name: value.get("name"),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn conflict_error() -> ApiError<'static> {
|
|
||||||
ApiError::new(StatusCode::CONFLICT, "conflict", "Conflict")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unique_violation(error: tokio_postgres::Error, api_error: fn() -> ApiError<'static>) -> Error {
|
fn unique_violation(error: tokio_postgres::Error, api_error: fn() -> ApiError<'static>) -> Error {
|
||||||
@ -39,11 +31,7 @@ fn unique_violation(error: tokio_postgres::Error, api_error: fn() -> ApiError<'s
|
|||||||
return error.into();
|
return error.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaginationType for User {
|
impl Users {
|
||||||
const NAME: &str = "UserList";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
pub async fn exists(
|
pub async fn exists(
|
||||||
client: &impl GenericClient,
|
client: &impl GenericClient,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@ -68,10 +56,10 @@ impl User {
|
|||||||
let id = Uuid::new_v7(Timestamp::now(NoContext));
|
let id = Uuid::new_v7(Timestamp::now(NoContext));
|
||||||
tx.execute(&stmt, &[&id, &name, &hash])
|
tx.execute(&stmt, &[&id, &name, &hash])
|
||||||
.await
|
.await
|
||||||
.map_err(|err| unique_violation(err, conflict_error))?;
|
.map_err(|err| unique_violation(err, || ApiError::CONFLICT))?;
|
||||||
Account::create(&mut tx, Some(id), id, "personal")
|
Accounts::create(&mut tx, Some(id), id, "personal")
|
||||||
.await
|
.await
|
||||||
.map_err(|err| unique_violation(err, conflict_error))?;
|
.map_err(|err| unique_violation(err, || ApiError::CONFLICT))?;
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
@ -87,7 +75,7 @@ impl User {
|
|||||||
let res = client.query_opt(&stmt, &[&name]).await?;
|
let res = client.query_opt(&stmt, &[&name]).await?;
|
||||||
Ok(res.map(|res| {
|
Ok(res.map(|res| {
|
||||||
let password = res.get("password");
|
let password = res.get("password");
|
||||||
(password, res.into())
|
(password, user_from_row(res))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
@ -99,7 +87,7 @@ impl User {
|
|||||||
.prepare_cached("select id,name from users where name = $1")
|
.prepare_cached("select id,name from users where name = $1")
|
||||||
.await?;
|
.await?;
|
||||||
let res = client.query_opt(&stmt, &[&name]).await?;
|
let res = client.query_opt(&stmt, &[&name]).await?;
|
||||||
Ok(res.map(User::from))
|
Ok(res.map(user_from_row))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(client))]
|
#[instrument(skip(client))]
|
||||||
@ -115,7 +103,7 @@ impl User {
|
|||||||
.query(&stmt, &[&pagination.limit(), &pagination.offset()])
|
.query(&stmt, &[&pagination.limit(), &pagination.offset()])
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(User::from)
|
.map(user_from_row)
|
||||||
.collect();
|
.collect();
|
||||||
let count = count(client, &stmt_count, &[]).await?;
|
let count = count(client, &stmt_count, &[]).await?;
|
||||||
Ok(Pagination::new(users, count, pagination))
|
Ok(Pagination::new(users, count, pagination))
|
||||||
@ -129,7 +117,7 @@ impl User {
|
|||||||
let stmt = client
|
let stmt = client
|
||||||
.prepare_cached("select id,name from users where id = $1")
|
.prepare_cached("select id,name from users where id = $1")
|
||||||
.await?;
|
.await?;
|
||||||
let info = client.query_opt(&stmt, &[&id]).await?.map(User::from);
|
let info = client.query_opt(&stmt, &[&id]).await?.map(user_from_row);
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user