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> { 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 { 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> for InteropAuth { type Rejection = StatusCode; async fn from_request_parts( parts: &mut Parts, state: &Arc, ) -> Result { 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, ) -> Result { 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, expires_in: u32, } 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;", ).await?; let accounts = conn .query(&stmt, &[]) .await? .into_iter() .map(|row| row.get(0)) .collect(); Ok(Json(ListUsers { users: accounts, expires_in: 42, })) }