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'
|
$ref: '#/components/responses/Unauthorized'
|
||||||
default:
|
default:
|
||||||
$ref: '#/components/responses/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:
|
/api/chats:
|
||||||
get:
|
get:
|
||||||
operationId: list-chats
|
operationId: list-chats
|
||||||
|
|||||||
@ -98,13 +98,13 @@ async fn interop_pay(
|
|||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
struct ListUsers {
|
pub struct InteropListUsers {
|
||||||
users: Vec<String>,
|
pub users: Vec<String>,
|
||||||
expires_in: u32,
|
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 conn = state.conn().await?;
|
||||||
let stmt = conn.prepare_cached(
|
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;",
|
"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()
|
.into_iter()
|
||||||
.map(|row| row.get(0))
|
.map(|row| row.get(0))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Json(ListUsers {
|
Ok(Json(InteropListUsers {
|
||||||
users: accounts,
|
users: accounts,
|
||||||
expires_in: 42,
|
expires_in: 42,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -11,6 +11,8 @@ use axum::{
|
|||||||
use bank_core::{ApiError, make_schemas, pagination::RequestPagination};
|
use bank_core::{ApiError, make_schemas, pagination::RequestPagination};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey};
|
use jsonwebtoken::{DecodingKey, EncodingKey};
|
||||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
|
use tokio::{sync::Mutex, time::Instant};
|
||||||
|
use tracing::error;
|
||||||
use tracing_error::SpanTrace;
|
use tracing_error::SpanTrace;
|
||||||
|
|
||||||
pub use axum::extract::State as EState;
|
pub use axum::extract::State as EState;
|
||||||
@ -28,6 +30,7 @@ mod transactions;
|
|||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
pub use interop::router as interop_router;
|
pub use interop::router as interop_router;
|
||||||
|
use transactions::TARGET_NOT_FOUND;
|
||||||
|
|
||||||
use crate::config::Meta;
|
use crate::config::Meta;
|
||||||
|
|
||||||
@ -104,6 +107,7 @@ pub enum InnerError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
#[track_caller]
|
||||||
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() {
|
||||||
Some(SpanTrace::capture())
|
Some(SpanTrace::capture())
|
||||||
@ -121,34 +125,56 @@ impl Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Into<InnerError>> From<T> for Error {
|
impl<T: Into<InnerError>> From<T> for Error {
|
||||||
|
#[track_caller]
|
||||||
fn from(value: T) -> Self {
|
fn from(value: T) -> Self {
|
||||||
Error::new(value.into())
|
Error::new(value.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<deadpool_postgres::PoolError> for InnerError {
|
impl From<deadpool_postgres::PoolError> for InnerError {
|
||||||
|
#[track_caller]
|
||||||
fn from(value: deadpool_postgres::PoolError) -> Self {
|
fn from(value: deadpool_postgres::PoolError) -> Self {
|
||||||
Self::Pool(value)
|
Self::Pool(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<tokio_postgres::Error> for InnerError {
|
impl From<tokio_postgres::Error> for InnerError {
|
||||||
|
#[track_caller]
|
||||||
fn from(value: tokio_postgres::Error) -> Self {
|
fn from(value: tokio_postgres::Error) -> Self {
|
||||||
Self::Postgres(value)
|
Self::Postgres(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<password_auth::ParseError> for InnerError {
|
impl From<password_auth::ParseError> for InnerError {
|
||||||
|
#[track_caller]
|
||||||
fn from(value: password_auth::ParseError) -> Self {
|
fn from(value: password_auth::ParseError) -> Self {
|
||||||
Self::PHCParse(value)
|
Self::PHCParse(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ApiError<'static>> for InnerError {
|
impl From<ApiError<'static>> for InnerError {
|
||||||
|
#[track_caller]
|
||||||
fn from(value: ApiError<'static>) -> Self {
|
fn from(value: ApiError<'static>) -> Self {
|
||||||
InnerError::Plain(value)
|
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 {
|
impl InnerError {
|
||||||
pub const fn internal(&self) -> bool {
|
pub const fn internal(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
@ -280,8 +306,20 @@ pub struct InteropState {
|
|||||||
pub secret: String,
|
pub secret: String,
|
||||||
pub client: reqwest::Client,
|
pub client: reqwest::Client,
|
||||||
pub url: url::Url,
|
pub url: url::Url,
|
||||||
|
pub user_url: url::Url,
|
||||||
pub pay_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 {
|
impl AppState {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use uuid::Uuid;
|
|||||||
use crate::model::{Accounts, Transactions, Users};
|
use crate::model::{Accounts, Transactions, Users};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppState, EState, Error, InteropState, Json, State,
|
AppState, EState, Error, InteropError, InteropState, Json, State,
|
||||||
auth::Auth,
|
auth::Auth,
|
||||||
socket::{SocketEvent, SocketMessage},
|
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,
|
StatusCode::NOT_FOUND,
|
||||||
"transaction.target.not_found",
|
"transaction.target.not_found",
|
||||||
"Not Found",
|
"Not Found",
|
||||||
@ -334,21 +334,7 @@ pub async fn make_payment(
|
|||||||
if amount % 100 != 0 {
|
if amount % 100 != 0 {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
if let Err(err) =
|
send_interop_payment(&interop, &from.name, &user, (amount / 100) as u32).await?;
|
||||||
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()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
transaction
|
transaction
|
||||||
}
|
}
|
||||||
AccountTarget::Selector(selector) => {
|
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(
|
async fn send_interop_payment(
|
||||||
interop: &InteropState,
|
interop: &InteropState,
|
||||||
from: &str,
|
from: &str,
|
||||||
@ -513,6 +493,7 @@ async fn send_interop_payment(
|
|||||||
let response = interop
|
let response = interop
|
||||||
.client
|
.client
|
||||||
.post(interop.pay_url.clone())
|
.post(interop.pay_url.clone())
|
||||||
|
.bearer_auth(&interop.secret)
|
||||||
.json(&serde_json::json!({
|
.json(&serde_json::json!({
|
||||||
"from": from,
|
"from": from,
|
||||||
"to": to,
|
"to": to,
|
||||||
@ -525,7 +506,7 @@ async fn send_interop_payment(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if response.status() == StatusCode::NOT_FOUND {
|
if response.status() == StatusCode::NOT_FOUND {
|
||||||
return Err(InteropError::NotFound);
|
return Err(InteropError::TransactionTargetNotFound);
|
||||||
}
|
}
|
||||||
Err(InteropError::Other(response))
|
Err(InteropError::Other(response))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use std::sync::Arc;
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
@ -11,18 +11,24 @@ use bank_core::{
|
|||||||
transaction::{FullTransaction, TransactionQuery},
|
transaction::{FullTransaction, TransactionQuery},
|
||||||
user::{User, UserAccounts, UserBalance},
|
user::{User, UserAccounts, UserBalance},
|
||||||
};
|
};
|
||||||
|
use concread::cowcell::asynch::CowCell;
|
||||||
|
use futures_util::lock::Mutex;
|
||||||
use garde::Validate;
|
use garde::Validate;
|
||||||
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
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 uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::ApiError,
|
api::{ApiError, InteropUsers, interop::InteropListUsers},
|
||||||
model::{Accounts, Transactions, Users},
|
model::{Accounts, Transactions, Users},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppState, EState, Error, Json, PaginationQuery, Query, State,
|
AppState, EState, Error, InteropError, InteropState, Json, PaginationQuery, Query, State,
|
||||||
auth::{Auth, Claims},
|
auth::{Auth, Claims},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,6 +42,7 @@ pub(super) fn router() -> Router<Arc<AppState>> {
|
|||||||
.route("/@me/transactions", get(me_transaction_history))
|
.route("/@me/transactions", get(me_transaction_history))
|
||||||
.route("/@me/password", put(change_password))
|
.route("/@me/password", put(change_password))
|
||||||
.route("/", get(list_users))
|
.route("/", get(list_users))
|
||||||
|
.route("/interop", get(list_interop_users))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
@ -90,6 +97,66 @@ pub async fn list_users(
|
|||||||
Ok(Json(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))]
|
#[instrument(skip(state))]
|
||||||
pub async fn me_transaction_history(
|
pub async fn me_transaction_history(
|
||||||
EState(state): State,
|
EState(state): State,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use bankserver::{
|
|||||||
setup_db,
|
setup_db,
|
||||||
};
|
};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey};
|
use jsonwebtoken::{DecodingKey, EncodingKey};
|
||||||
use tokio::{net::TcpListener, signal};
|
use tokio::{net::TcpListener, signal, sync::Mutex};
|
||||||
use tracing::{info, level_filters::LevelFilter};
|
use tracing::{info, level_filters::LevelFilter};
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt};
|
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt};
|
||||||
@ -45,9 +45,11 @@ async fn main() {
|
|||||||
.timeout(Duration::from_secs(2))
|
.timeout(Duration::from_secs(2))
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
user_url: config.url.join("/_internal/users").unwrap(),
|
||||||
pay_url: config.url.join("/_internal/pay").unwrap(),
|
pay_url: config.url.join("/_internal/pay").unwrap(),
|
||||||
url: config.url,
|
url: config.url,
|
||||||
prefix: config.prefix,
|
suffix: config.prefix,
|
||||||
|
users: Mutex::new(None),
|
||||||
});
|
});
|
||||||
|
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user