diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index e507c78..fd70078 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -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: diff --git a/.gitignore b/.gitignore index dad01e9..13284d1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /result-* /result /schemas +openapi.json diff --git a/justfile b/justfile index 35bd190..93ba81b 100644 --- a/justfile +++ b/justfile @@ -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 diff --git a/openapi-def.yaml b/openapi-def.yaml index 5a847f5..1e11177 100644 --- a/openapi-def.yaml +++ b/openapi-def.yaml @@ -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: diff --git a/openapi.json b/openapi.json deleted file mode 100644 index 8b13789..0000000 --- a/openapi.json +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/api/auth.rs b/src/api/auth.rs index 7fb5672..cb61d5e 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -6,7 +6,7 @@ use std::{ use axum::{ Router, - extract::FromRequestParts, + extract::{FromRef, FromRequestParts}, http::{StatusCode, request::Parts}, routing::post, }; diff --git a/src/api/docs.rs b/src/api/docs.rs index e043fbe..9ee6442 100644 --- a/src/api/docs.rs +++ b/src/api/docs.rs @@ -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> { 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) +} diff --git a/src/api/docs/index.html b/src/api/docs/index.html new file mode 100644 index 0000000..a13c1bb --- /dev/null +++ b/src/api/docs/index.html @@ -0,0 +1,25 @@ + + + + + API Reference + + + + + + + + + + + \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index c763b74..3bf886c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -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"), diff --git a/src/api/user.rs b/src/api/user.rs index a78bc57..f3fbf2b 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -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> { 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) { +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 = 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, 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, 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) {} diff --git a/src/model/user.rs b/src/model/user.rs index 8bef270..678a4e0 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -93,4 +93,16 @@ impl User { .collect(); Ok(users) } + + #[instrument(skip(client))] + pub async fn info( + client: &impl GenericClient, + id: Uuid, + ) -> Result, 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) + } }