diff --git a/openapi-def.yaml b/openapi-def.yaml index d9cb668..7ecbfbf 100644 --- a/openapi-def.yaml +++ b/openapi-def.yaml @@ -425,6 +425,29 @@ paths: $ref: '#/components/responses/Unauthorized' default: $ref: '#/components/responses/Default' + /api/users/interop: + get: + operationId: users-list-all-interop + summary: List all remote users + tags: + - Users + security: + - bearer: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + type: array + items: + type: string + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + default: + $ref: '#/components/responses/Default' /api/chats: get: operationId: list-chats diff --git a/src/api/interop.rs b/src/api/interop.rs index 5262bc7..1c3c069 100644 --- a/src/api/interop.rs +++ b/src/api/interop.rs @@ -98,13 +98,13 @@ async fn interop_pay( Ok(StatusCode::OK) } -#[derive(Serialize)] -struct ListUsers { - users: Vec, - expires_in: u32, +#[derive(Deserialize, Serialize)] +pub struct InteropListUsers { + pub users: Vec, + pub expires_in: u32, } -async fn list_users(EState(state): State, _: InteropAuth) -> Result, Error> { +async fn list_users(EState(state): State, _: InteropAuth) -> Result, Error> { let conn = state.conn().await?; let stmt = conn.prepare_cached( "select case when a.name = 'personal' then u.name else concat(u.name, ':', a.name) end as account from accounts a join users u ON a.\"user\" = u.id;", @@ -115,7 +115,7 @@ async fn list_users(EState(state): State, _: InteropAuth) -> Result Self { let trace = if cfg!(debug_assertions) || inner.internal() { Some(SpanTrace::capture()) @@ -121,34 +125,56 @@ impl Error { } impl> From for Error { + #[track_caller] fn from(value: T) -> Self { Error::new(value.into()) } } impl From for InnerError { + #[track_caller] fn from(value: deadpool_postgres::PoolError) -> Self { Self::Pool(value) } } impl From for InnerError { + #[track_caller] fn from(value: tokio_postgres::Error) -> Self { Self::Postgres(value) } } impl From for InnerError { + #[track_caller] fn from(value: password_auth::ParseError) -> Self { Self::PHCParse(value) } } impl From> for InnerError { + #[track_caller] fn from(value: ApiError<'static>) -> Self { InnerError::Plain(value) } } +impl From for InnerError { + #[track_caller] + fn from(value: InteropError) -> Self { + match value { + InteropError::TransactionTargetNotFound => TARGET_NOT_FOUND.into(), + InteropError::Http(err) => { + error!("{err}"); + ApiError::INTERNAL_SERVER_ERROR.into() + } + InteropError::Other(response) => { + error!("{response:?}"); + ApiError::INTERNAL_SERVER_ERROR.into() + } + } + } +} + impl InnerError { pub const fn internal(&self) -> bool { match self { @@ -280,8 +306,20 @@ pub struct InteropState { pub secret: String, pub client: reqwest::Client, pub url: url::Url, + pub user_url: url::Url, pub pay_url: url::Url, - pub prefix: String, + pub suffix: String, + pub users: Mutex>, +} + +pub enum InteropError { + TransactionTargetNotFound, + Http(reqwest::Error), + Other(reqwest::Response), +} + +pub struct InteropUsers { + names: Vec, } impl AppState { diff --git a/src/api/transactions.rs b/src/api/transactions.rs index bdde3bc..90942f5 100644 --- a/src/api/transactions.rs +++ b/src/api/transactions.rs @@ -13,7 +13,7 @@ use uuid::Uuid; use crate::model::{Accounts, Transactions, Users}; use super::{ - AppState, EState, Error, InteropState, Json, State, + AppState, EState, Error, InteropError, InteropState, Json, State, auth::Auth, socket::{SocketEvent, SocketMessage}, }; @@ -296,7 +296,7 @@ impl MakePayment { } } -const TARGET_NOT_FOUND: ApiError<'static> = ApiError::const_new( +pub const TARGET_NOT_FOUND: ApiError<'static> = ApiError::const_new( StatusCode::NOT_FOUND, "transaction.target.not_found", "Not Found", @@ -334,21 +334,7 @@ pub async fn make_payment( if amount % 100 != 0 { todo!() } - if let Err(err) = - send_interop_payment(&interop, &from.name, &user, (amount / 100) as u32).await - { - return Err(match err { - InteropError::NotFound => TARGET_NOT_FOUND.into(), - InteropError::Http(err) => { - error!("{err}"); - ApiError::INTERNAL_SERVER_ERROR.into() - } - InteropError::Other(response) => { - error!("{response:?}"); - ApiError::INTERNAL_SERVER_ERROR.into() - } - }); - } + send_interop_payment(&interop, &from.name, &user, (amount / 100) as u32).await?; transaction } AccountTarget::Selector(selector) => { @@ -498,12 +484,6 @@ impl<'a, T: GenericClient> TransactionBuilder<'a, T> { } } -enum InteropError { - NotFound, - Http(reqwest::Error), - Other(reqwest::Response), -} - async fn send_interop_payment( interop: &InteropState, from: &str, @@ -513,6 +493,7 @@ async fn send_interop_payment( let response = interop .client .post(interop.pay_url.clone()) + .bearer_auth(&interop.secret) .json(&serde_json::json!({ "from": from, "to": to, @@ -525,7 +506,7 @@ async fn send_interop_payment( return Ok(()); } if response.status() == StatusCode::NOT_FOUND { - return Err(InteropError::NotFound); + return Err(InteropError::TransactionTargetNotFound); } Err(InteropError::Other(response)) } diff --git a/src/api/user.rs b/src/api/user.rs index 0c1894d..ca9a4cf 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use axum::{ Router, @@ -11,18 +11,24 @@ use bank_core::{ transaction::{FullTransaction, TransactionQuery}, user::{User, UserAccounts, UserBalance}, }; +use concread::cowcell::asynch::CowCell; +use futures_util::lock::Mutex; use garde::Validate; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; -use tracing::instrument; +use serde_json::json; +use tokio::time::Instant; +use tracing::{error, instrument}; +use url::Url; use uuid::Uuid; use crate::{ - api::ApiError, + api::{ApiError, InteropUsers, interop::InteropListUsers}, model::{Accounts, Transactions, Users}, }; use super::{ - AppState, EState, Error, Json, PaginationQuery, Query, State, + AppState, EState, Error, InteropError, InteropState, Json, PaginationQuery, Query, State, auth::{Auth, Claims}, }; @@ -36,6 +42,7 @@ pub(super) fn router() -> Router> { .route("/@me/transactions", get(me_transaction_history)) .route("/@me/password", put(change_password)) .route("/", get(list_users)) + .route("/interop", get(list_interop_users)) } #[derive(Deserialize, Serialize)] @@ -90,6 +97,66 @@ pub async fn list_users( Ok(Json(users)) } +async fn fetch_users(state: &InteropState) -> Result, InteropError> { + let response = state + .client + .get(state.user_url.clone()) + .bearer_auth(&state.secret) + .send() + .await + .map_err(InteropError::Http)?; + if response.status() != StatusCode::OK { + return Err(InteropError::Other(response)); + } + let mut data = response + .json::() + .await + .map_err(InteropError::Http)?; + for name in data.users.iter_mut() { + name.push('@'); + name.push_str(&state.suffix); + } + Ok(data.users) +} + +#[instrument(skip(state))] +pub async fn list_interop_users( + EState(state): State, + _: Auth, +) -> Result, Error> { + let Some(interop) = &state.interop else { + return Ok(Json(serde_json::Value::Array(vec![]))); + }; + let mut guard = interop.users.lock().await; + let names = match &mut *guard { + Some((users, time)) => { + if Instant::now().duration_since(*time) > Duration::from_secs(100) { + match fetch_users(interop).await { + Ok(names) => { + users.names = names; + } + Err(err) => { + let _ = Error::from(err); + } + }; + }; + users.names.as_slice() + } + None => { + match fetch_users(interop).await { + Ok(names) => { + *guard = Some((InteropUsers { names }, Instant::now())); + } + Err(err) => { + let _ = Error::from(err); + } + }; + guard.as_ref().unwrap().0.names.as_slice() + } + }; + Ok(Json(json!(names))) +} + #[instrument(skip(state))] pub async fn me_transaction_history( EState(state): State, diff --git a/src/main.rs b/src/main.rs index 31b00f0..933d4b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use bankserver::{ setup_db, }; use jsonwebtoken::{DecodingKey, EncodingKey}; -use tokio::{net::TcpListener, signal}; +use tokio::{net::TcpListener, signal, sync::Mutex}; use tracing::{info, level_filters::LevelFilter}; use tracing_error::ErrorLayer; use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt}; @@ -45,9 +45,11 @@ async fn main() { .timeout(Duration::from_secs(2)) .build() .unwrap(), + user_url: config.url.join("/_internal/users").unwrap(), pay_url: config.url.join("/_internal/pay").unwrap(), url: config.url, - prefix: config.prefix, + suffix: config.prefix, + users: Mutex::new(None), }); let router = Router::new()