bankserver_rust/src/api/interop.rs
2025-03-24 16:14:33 +01:00

123 lines
3.3 KiB
Rust

use std::sync::Arc;
use axum::{
Router,
extract::FromRequestParts,
http::request::Parts,
routing::{get, post},
};
use bank_core::{NAME_PATTERN_RE, Name, NameOrUuid, USER_ACCOUNT_PATTERN_RE};
use garde::Validate;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::api::auth::auth_header;
use super::{
AppState, EState, Error, Json, State,
transactions::{AccountSelector, TransactionBuilder},
};
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/_internal/pay", post(interop_pay))
.route("/_internal/users", get(list_users))
}
#[derive(Debug, Deserialize, Validate)]
struct InteropPayBody {
#[garde(pattern(NAME_PATTERN_RE))]
from: String,
#[garde(pattern(USER_ACCOUNT_PATTERN_RE))]
to: String,
#[garde(skip)]
amount: u64,
}
fn parse_target(target: &str) -> Result<AccountSelector, garde::Error> {
let mut target = target.splitn(2, ':');
let user = target.next().unwrap();
Name::validate_name(user)?;
let user = NameOrUuid::Name(Name(user.into()));
let Some(account) = target.next() else {
return Ok(AccountSelector {
user,
account: None,
});
};
Name::validate_name(account)?;
Ok(AccountSelector {
user,
account: Some(Name(account.into())),
})
}
struct InteropAuth;
impl FromRequestParts<Arc<AppState>> for InteropAuth {
type Rejection = StatusCode;
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<AppState>,
) -> Result<Self, Self::Rejection> {
let key = auth_header(parts).map_err(|_| StatusCode::UNAUTHORIZED)?;
let Some(interop) = &state.interop else {
return Err(StatusCode::UNAUTHORIZED);
};
if interop.secret != key {
return Err(StatusCode::UNAUTHORIZED);
}
Ok(Self)
}
}
#[instrument(skip(state))]
async fn interop_pay(
EState(state): State,
_: InteropAuth,
Json(body): Json<InteropPayBody>,
) -> Result<StatusCode, Error> {
let target = parse_target(&body.to).map_err(|err| {
let mut report = garde::Report::new();
report.append(garde::Path::new("to"), err);
report
})?;
let mut client = state.conn().await?;
let mut client = client.transaction().await?;
let Some((user, account)) = target.account_id(&mut client).await? else {
return Ok(StatusCode::NOT_FOUND);
};
let (_, notification) = TransactionBuilder::new(&mut client, body.amount)
.await?
.interop_receive(account, body.from, None)
.await?;
client.commit().await?;
state.sockets.send(user, notification).await;
Ok(StatusCode::OK)
}
#[derive(Serialize)]
struct ListUsers {
users: Vec<String>,
expires_in: u32,
}
async fn list_users(EState(state): State, _: InteropAuth) -> Result<Json<ListUsers>, 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;",
).await?;
let accounts = conn
.query(&stmt, &[])
.await?
.into_iter()
.map(|row| row.get(0))
.collect();
Ok(Json(ListUsers {
users: accounts,
expires_in: 42,
}))
}