From 975fc1d5674b49315f1c0f4cac55d5def44192c1 Mon Sep 17 00:00:00 2001 From: DSeeLP <46624152+DSeeLP@users.noreply.github.com> Date: Mon, 24 Mar 2025 15:24:53 +0100 Subject: [PATCH] implement custom data --- migrations/000002_user_data.sql | 6 ++++++ openapi-def.yaml | 36 ++++++++++++++++++++++++++++++++ src/api/interop.rs | 2 +- src/api/user.rs | 27 ++++++++++++++++++++++++ src/model/user.rs | 34 ++++++++++++++++++++++++++++++ tests/integration/user_data.hurl | 23 ++++++++++++++++++++ 6 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 migrations/000002_user_data.sql create mode 100644 tests/integration/user_data.hurl diff --git a/migrations/000002_user_data.sql b/migrations/000002_user_data.sql new file mode 100644 index 0000000..419de3e --- /dev/null +++ b/migrations/000002_user_data.sql @@ -0,0 +1,6 @@ +create table user_data( + owner uuid not null references users(id) on delete cascade, + "user" uuid not null references users(id) on delete cascade, + data jsonb not null, + primary key (owner, "user") +); diff --git a/openapi-def.yaml b/openapi-def.yaml index 154dbcc..76029c1 100644 --- a/openapi-def.yaml +++ b/openapi-def.yaml @@ -125,6 +125,42 @@ paths: $ref: '#/components/responses/Unauthorized' default: $ref: '#/components/responses/Default' + /api/users/@me/data: + get: + operationId: self-get-data + summary: User data + tags: + - Users + security: + - bearer: [] + responses: + 200: + description: Ok + content: + application/json: + schema: {} + 401: + $ref: '#/components/responses/Unauthorized' + default: + $ref: '#/components/responses/Default' + post: + operationId: self-set-data + summary: Set User data + tags: + - Users + security: + - bearer: [] + requestBody: + content: + application/json: + schema: {} + responses: + 200: + description: Ok + 401: + $ref: '#/components/responses/Unauthorized' + default: + $ref: '#/components/responses/Default' /api/users/@me/balance: get: operationId: self-get-balance diff --git a/src/api/interop.rs b/src/api/interop.rs index 567e961..7000db4 100644 --- a/src/api/interop.rs +++ b/src/api/interop.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; use axum::{ Router, diff --git a/src/api/user.rs b/src/api/user.rs index f22b4fb..f430be4 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -21,6 +21,7 @@ pub(super) fn router() -> Router> { Router::new() .route("/{target}", get(user_info)) .route("/@me/balance", get(user_balance)) + .route("/@me/data", get(get_user_data).post(set_user_data)) .route("/@me/accounts", get(user_accounts)) .route("/@me/transactions", get(me_transaction_history)) .route("/", get(list_users)) @@ -100,6 +101,32 @@ pub async fn user_accounts(EState(state): State, auth: Auth) -> Result Result>, Error> { + let user = auth.user_id(); + let conn = state.conn().await?; + let data = Users::get_data(&conn, user).await?; + Ok(Json(data)) +} + +#[instrument(skip(state))] +pub async fn set_user_data( + EState(state): State, + auth: Auth, + 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, data).await?, + None => Users::delete_data(&mut conn, user).await?, + } + Ok(()) +} + #[cfg(test)] mod tests { use uuid::Uuid; diff --git a/src/model/user.rs b/src/model/user.rs index f658e39..aff9831 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -120,4 +120,38 @@ impl Users { let info = client.query_opt(&stmt, &[&id]).await?.map(user_from_row); Ok(info) } + + pub async fn get_data( + client: &impl GenericClient, + user: Uuid, + ) -> Result, tokio_postgres::Error> { + let stmt = client + .prepare_cached("select data from user_data where owner = $1 and \"user\" = $1") + .await?; + let res = client.query_opt(&stmt, &[&user]).await?; + Ok(res.map(|row| row.get(0))) + } + + pub async fn set_data( + client: &mut impl GenericClient, + user: Uuid, + data: serde_json::Value, + ) -> Result<(), tokio_postgres::Error> { + let stmt = client + .prepare_cached("insert into user_data(owner, \"user\", data) values ($1, $1, $2) on conflict (owner, \"user\") do update set data = $2") + .await?; + client.execute(&stmt, &[&user, &data]).await?; + Ok(()) + } + + pub async fn delete_data( + client: &mut impl GenericClient, + user: Uuid, + ) -> Result<(), tokio_postgres::Error> { + let stmt = client + .prepare_cached("delete from user_data where \"owner\" = $1 and user = $2") + .await?; + client.execute(&stmt, &[&user]).await?; + Ok(()) + } } diff --git a/tests/integration/user_data.hurl b/tests/integration/user_data.hurl new file mode 100644 index 0000000..58d7ecd --- /dev/null +++ b/tests/integration/user_data.hurl @@ -0,0 +1,23 @@ +POST {{host}}/api/login +{ + "name": "user1", + "password": "this-is-a-password" +} +HTTP 200 + +[Captures] +token: jsonpath "$.token" + +POST {{host}}/api/users/@me/data +Authorization: Bearer {{token}} +{ + "hello": "world" +} +HTTP 200 + +GET {{host}}/api/users/@me/data +Authorization: Bearer {{token}} +HTTP 200 + +[Asserts] +jsonpath "$.hello" == "world"