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 garde::Validate; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::time::Instant; use tracing::instrument; 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> { 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, ) -> Result, 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, 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>, Error> { let conn = state.conn().await?; let users = Users::list(&conn, pagination).await?; Ok(Json(users)) } async fn fetch_users(state: &InteropState) -> Result, 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::() .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, 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, ) -> Result>, 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, 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>, 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, ) -> Result>, 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, Json(body): Json>, ) -> 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, ) -> Result, 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""#); } }