mirror of
https://git.dirksys.ovh/dirk/bankserver.git
synced 2025-12-20 02:59:20 +01:00
246 lines
6.9 KiB
Rust
246 lines
6.9 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 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<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""#);
|
|
}
|
|
}
|