mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 02:59:20 +01:00
implement endpoint to list remote users
This commit is contained in:
parent
9b1368dd9c
commit
ce13d854c1
@ -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
|
||||
|
||||
@ -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,
|
||||
}))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user