2025-04-17 10:25:45 +02:00

249 lines
7.0 KiB
Rust

use std::{sync::Arc, time::Duration};
use axum::{
Router,
extract::Path,
routing::{get, put},
};
use bank_core::{
ChangePassword, TokenResponse,
pagination::Pagination,
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 serde_json::json;
use tokio::time::Instant;
use tracing::{error, instrument};
use url::Url;
use uuid::Uuid;
use crate::{
api::{ApiError, InteropUsers, interop::InteropListUsers},
model::{Accounts, Transactions, Users},
};
use super::{
AppState, EState, Error, InteropError, InteropState, Json, PaginationQuery, Query, State,
auth::{Auth, Claims},
};
pub(super) fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/{target}", get(user_info))
.route("/@me/balance", get(user_balance))
.route("/@me/data", get(list_user_data_keys))
.route("/@me/data/{key}", get(get_user_data).put(set_user_data))
.route("/@me/accounts", get(user_accounts))
.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)]
pub enum UserTarget {
#[serde(rename = "@me")]
Me,
Id(Uuid),
}
impl UserTarget {
pub fn user_id(&self, auth: &Auth) -> Uuid {
match self {
UserTarget::Me => auth.user_id(),
UserTarget::Id(uuid) => *uuid,
}
}
}
pub async fn user_info(
EState(state): State,
auth: Auth,
Path(target): Path<UserTarget>,
) -> Result<Json<User>, Error> {
let user = target.user_id(&auth);
let conn = state.conn().await?;
let info = Users::info(&conn, user).await?;
if let Some(info) = info {
return Ok(Json(info));
}
if matches!(target, UserTarget::Me) {
return Err(ApiError::INTERNAL_SERVER_ERROR.into());
}
Err(ApiError::NOT_FOUND.into())
}
#[instrument(skip(state))]
pub async fn user_balance(EState(state): State, auth: Auth) -> Result<Json<UserBalance>, Error> {
let conn = state.conn().await?;
let info = Accounts::list_for_user(&conn, auth.user_id()).await?;
let balance = info.iter().map(|info| info.balance).sum();
Ok(Json(UserBalance { balance }))
}
#[instrument(skip(state))]
pub async fn list_users(
EState(state): State,
_: Auth,
PaginationQuery(pagination): PaginationQuery,
) -> Result<Json<Pagination<User>>, Error> {
let conn = state.conn().await?;
let users = Users::list(&conn, pagination).await?;
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,
auth: Auth,
Query(TransactionQuery {
direction,
pagination,
}): Query<TransactionQuery>,
) -> Result<Json<Pagination<FullTransaction>>, Error> {
let conn = state.conn().await?;
let result = Transactions::user_history(&conn, auth.user_id(), direction, pagination).await?;
Ok(Json(result))
}
#[instrument(skip(state))]
pub async fn user_accounts(EState(state): State, auth: Auth) -> Result<Json<UserAccounts>, Error> {
let user = auth.user_id();
let conn = state.conn().await?;
let result = Accounts::list_for_user(&conn, user).await?;
Ok(Json(UserAccounts { result }))
}
#[instrument(skip(state))]
pub async fn list_user_data_keys(
EState(state): State,
auth: Auth,
) -> Result<Json<Vec<String>>, Error> {
let user = auth.user_id();
let conn = state.conn().await?;
let data = Users::list_data(&conn, user).await?;
Ok(Json(data))
}
#[instrument(skip(state))]
pub async fn get_user_data(
EState(state): State,
auth: Auth,
Path(key): Path<String>,
) -> Result<Json<Option<serde_json::Value>>, Error> {
let user = auth.user_id();
let conn = state.conn().await?;
let data = Users::get_data(&conn, user, &key).await?;
Ok(Json(data))
}
#[instrument(skip(state))]
pub async fn set_user_data(
EState(state): State,
auth: Auth,
Path(key): Path<String>,
Json(body): Json<Option<serde_json::Value>>,
) -> Result<(), Error> {
let user = auth.user_id();
let mut conn = state.conn().await?;
match body {
Some(data) => Users::set_data(&mut conn, user, &key, data).await?,
None => Users::delete_data(&mut conn, user, &key).await?,
}
Ok(())
}
#[instrument(skip(state))]
async fn change_password(
EState(state): State,
auth: Auth,
Json(body): Json<ChangePassword>,
) -> Result<Json<TokenResponse>, Error> {
body.validate()?;
let mut conn = state.conn().await?;
let id = auth.user_id();
Users::change_password(&mut conn, id, &body.password).await?;
let token = Claims::new(id).encode(&state.encoding_key).unwrap();
Ok(Json(TokenResponse { token }))
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::UserTarget;
#[test]
fn user_target() {
let json = serde_json::to_string(&UserTarget::Me).unwrap();
assert_eq!(json, r#""@me""#);
let json = serde_json::to_string(&Uuid::nil()).unwrap();
assert_eq!(json, r#""00000000-0000-0000-0000-000000000000""#);
}
}