implement endpoint to list remote users

This commit is contained in:
DSeeLP 2025-04-17 10:19:18 +02:00
parent 9b1368dd9c
commit ce13d854c1
6 changed files with 148 additions and 37 deletions

View File

@ -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

View File

@ -98,13 +98,13 @@ async fn interop_pay(
Ok(StatusCode::OK)
}
#[derive(Serialize)]
struct ListUsers {
users: Vec<String>,
expires_in: u32,
#[derive(Deserialize, Serialize)]
pub struct InteropListUsers {
pub users: Vec<String>,
pub expires_in: u32,
}
async fn list_users(EState(state): State, _: InteropAuth) -> Result<Json<ListUsers>, Error> {
async fn list_users(EState(state): State, _: InteropAuth) -> Result<Json<InteropListUsers>, 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<Json<ListUse
.into_iter()
.map(|row| row.get(0))
.collect();
Ok(Json(ListUsers {
Ok(Json(InteropListUsers {
users: accounts,
expires_in: 42,
}))

View File

@ -11,6 +11,8 @@ use axum::{
use bank_core::{ApiError, make_schemas, pagination::RequestPagination};
use jsonwebtoken::{DecodingKey, EncodingKey};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tokio::{sync::Mutex, time::Instant};
use tracing::error;
use tracing_error::SpanTrace;
pub use axum::extract::State as EState;
@ -28,6 +30,7 @@ mod transactions;
mod user;
pub use interop::router as interop_router;
use transactions::TARGET_NOT_FOUND;
use crate::config::Meta;
@ -104,6 +107,7 @@ pub enum InnerError {
}
impl Error {
#[track_caller]
pub fn new(inner: InnerError) -> Self {
let trace = if cfg!(debug_assertions) || inner.internal() {
Some(SpanTrace::capture())
@ -121,34 +125,56 @@ impl Error {
}
impl<T: Into<InnerError>> From<T> for Error {
#[track_caller]
fn from(value: T) -> Self {
Error::new(value.into())
}
}
impl From<deadpool_postgres::PoolError> for InnerError {
#[track_caller]
fn from(value: deadpool_postgres::PoolError) -> Self {
Self::Pool(value)
}
}
impl From<tokio_postgres::Error> for InnerError {
#[track_caller]
fn from(value: tokio_postgres::Error) -> Self {
Self::Postgres(value)
}
}
impl From<password_auth::ParseError> for InnerError {
#[track_caller]
fn from(value: password_auth::ParseError) -> Self {
Self::PHCParse(value)
}
}
impl From<ApiError<'static>> for InnerError {
#[track_caller]
fn from(value: ApiError<'static>) -> Self {
InnerError::Plain(value)
}
}
impl From<InteropError> 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<Option<(InteropUsers, Instant)>>,
}
pub enum InteropError {
TransactionTargetNotFound,
Http(reqwest::Error),
Other(reqwest::Response),
}
pub struct InteropUsers {
names: Vec<String>,
}
impl AppState {

View File

@ -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))
}

View File

@ -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<Arc<AppState>> {
.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<Vec<String>, 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::<InteropListUsers>()
.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<Json<serde_json::Value>, 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,

View File

@ -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()