mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-21 19:49:24 +01:00
123 lines
3.3 KiB
Rust
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,
|
|
}))
|
|
}
|