implement user info, user balance and list_users endpoint

This commit is contained in:
DSeeLP 2025-03-03 13:34:10 +01:00
parent 9a52d120bf
commit 01629f00b5
11 changed files with 91 additions and 11 deletions

View File

@ -65,7 +65,7 @@ jobs:
done
echo "Pushing manifest"
podman login --get-login git.dirksys.ovh
podman --log-level debug manifest push git.dirksys.ovh/dirk/bankserver:latest
podman manifest push git.dirksys.ovh/dirk/bankserver:latest
- name: Notify server
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
env:

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/result-*
/result
/schemas
openapi.json

View File

@ -4,7 +4,7 @@ default:
dev:
just openapi
cargo run
cargo run --bin bankserver
openapi:
yq eval-all -n 'load("openapi-def.yaml") *n load("schemas/schemas.json")' > openapi-temp.yaml

View File

@ -74,6 +74,8 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UserInfo'
404:
$ref: '#/components/responses/ResourceNotFound'
401:
$ref: '#/components/responses/Unauthorized'
default:
@ -289,6 +291,15 @@ components:
value:
id: auth.jwt.invalid
message: string
ResourceNotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
example:
id: not-found
message: Not found
UnprocessableEntity:
description: Unprocessable Entity
content:

View File

@ -1 +0,0 @@

View File

@ -6,7 +6,7 @@ use std::{
use axum::{
Router,
extract::FromRequestParts,
extract::{FromRef, FromRequestParts},
http::{StatusCode, request::Parts},
routing::post,
};

View File

@ -14,9 +14,11 @@ static OPENAPI_JSON: &'static str = include_str!("../../openapi.json");
static SCALAR_DOCS: &'static str = include_str!("./docs/scalar.html");
static SWAGGER_DOCS: &'static str = include_str!("./docs/swagger.html");
static RAPIDOC_DOCS: &'static str = include_str!("./docs/rapidoc.html");
static INDEX: &'static str = include_str!("./docs/index.html");
pub(super) fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/", get(index_html))
.route("/openapi.json", get(openapi_json))
.route("/scalar", get(scalar_html))
.route("/swagger", get(swagger_html))
@ -44,3 +46,6 @@ async fn swagger_html() -> Html<&'static str> {
async fn rapidoc_html() -> Html<&'static str> {
Html(RAPIDOC_DOCS)
}
async fn index_html() -> Html<&'static str> {
Html(INDEX)
}

25
src/api/docs/index.html Normal file
View File

@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<title>API Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
font-family: sans-serif;
color-scheme: light dark;
}
</style>
</head>
<body>
<ul>
<li><a href="./docs/swagger">Swagger</a></li>
<li><a href="./docs/scalar">Scalar</a></li>
<li><a href="./docs/rapidoc">Rapidoc</a></li>
</ul>
</body>
</html>

View File

@ -175,7 +175,7 @@ impl IntoResponse for InnerError {
}
}
static INTERNAL_SERVER_ERROR: ApiError<'static> = ApiError {
pub static INTERNAL_SERVER_ERROR: ApiError<'static> = ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
id: Cow::Borrowed("internal_server_error"),
message: Cow::Borrowed("Internal Server Error"),

View File

@ -1,12 +1,17 @@
use std::sync::Arc;
use axum::{Router, extract::Path, routing::get};
use axum::{Router, extract::Path, http::StatusCode, routing::get};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::model::{UserAccountInfo, UserInfo};
use crate::{
api::ApiError,
model::{Account, User, UserAccountInfo, UserInfo},
};
use super::{AppState, EState, State, auth::Auth, make_schemas};
use super::{
AppState, EState, Error, INTERNAL_SERVER_ERROR, Json, State, auth::Auth, make_schemas,
};
pub(super) fn router() -> Router<Arc<AppState>> {
Router::new()
@ -52,8 +57,30 @@ pub struct UserBalance {
pub balance: u64,
}
pub async fn user_info(EState(state): State, auth: Auth, Path(target): Path<UserTarget>) {
pub async fn user_info(
EState(state): State,
auth: Auth,
Path(target): Path<UserTarget>,
) -> Result<Json<UserInfo>, Error> {
let user = target.user_id(&auth);
let conn = state.conn().await?;
let info = User::info(&conn, user).await?;
if let Some(info) = info {
return Ok(Json(info));
}
if matches!(target, UserTarget::Me) {
return Err(INTERNAL_SERVER_ERROR.clone().into());
}
Err(ApiError::const_new(StatusCode::NOT_FOUND, "not.found", "Not found").into())
}
pub async fn user_balance(EState(state): State, auth: Auth) -> Result<Json<UserBalance>, Error> {
let conn = state.conn().await?;
let info = Account::list_for_user(&conn, auth.user_id()).await?;
let balance = info.iter().map(|info| info.balance).sum();
Ok(Json(UserBalance { balance }))
}
pub async fn list_users(EState(state): State, _: Auth) -> Result<Json<ListUsers>, Error> {
let conn = state.conn().await?;
let users = User::list(&conn).await?;
Ok(Json(ListUsers { users }))
}
pub async fn user_balance(EState(state): State, auth: Auth) {}
pub async fn list_users(EState(state): State, _: Auth) {}

View File

@ -93,4 +93,16 @@ impl User {
.collect();
Ok(users)
}
#[instrument(skip(client))]
pub async fn info(
client: &impl GenericClient,
id: Uuid,
) -> Result<Option<UserInfo>, tokio_postgres::Error> {
let stmt = client
.prepare_cached("select id,name from users where id = $1")
.await?;
let info = client.query_opt(&stmt, &[]).await?.map(UserInfo::from);
Ok(info)
}
}