key based user data

This commit is contained in:
DSeeLP 2025-03-24 18:54:39 +01:00
parent ac695a4b16
commit e8eef372aa
4 changed files with 100 additions and 13 deletions

View File

@ -1,6 +1,8 @@
create table user_data(
owner uuid not null references users(id) on delete cascade,
"user" uuid not null references users(id) on delete cascade,
"key" text not null,
data jsonb not null,
primary key (owner, "user")
unique (owner, "user"),
primary key (owner, "user", "key")
);

View File

@ -21,7 +21,8 @@ pub(super) fn router() -> Router<Arc<AppState>> {
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/data", get(list_user_data_keys))
.route("/@me/data/{key}", 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))
@ -101,14 +102,26 @@ pub async fn user_accounts(EState(state): State, auth: Auth) -> Result<Json<User
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).await?;
let data = Users::get_data(&conn, user, &key).await?;
Ok(Json(data))
}
@ -116,13 +129,14 @@ pub async fn get_user_data(
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, data).await?,
None => Users::delete_data(&mut conn, user).await?,
Some(data) => Users::set_data(&mut conn, user, &key, data).await?,
None => Users::delete_data(&mut conn, user, &key).await?,
}
Ok(())
}

View File

@ -121,37 +121,64 @@ impl Users {
Ok(info)
}
#[instrument(skip(client))]
pub async fn list_data(
client: &impl GenericClient,
user: Uuid,
) -> Result<Vec<String>, tokio_postgres::Error> {
let stmt = client
.prepare_cached("select \"key\" from user_data where owner = $1 and \"user\" = $1")
.await?;
let res = client
.query(&stmt, &[&user])
.await?
.into_iter()
.map(|row| row.get(0))
.collect();
Ok(res)
}
#[instrument(skip(client))]
pub async fn get_data(
client: &impl GenericClient,
user: Uuid,
key: &str,
) -> Result<Option<serde_json::Value>, tokio_postgres::Error> {
let stmt = client
.prepare_cached("select data from user_data where owner = $1 and \"user\" = $1")
.prepare_cached(
"select data from user_data where owner = $1 and \"user\" = $1 and \"key\" = $2 ",
)
.await?;
let res = client.query_opt(&stmt, &[&user]).await?;
let res = client.query_opt(&stmt, &[&user, &key]).await?;
Ok(res.map(|row| row.get(0)))
}
#[instrument(skip(client, data))]
pub async fn set_data(
client: &mut impl GenericClient,
user: Uuid,
key: &str,
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")
.prepare_cached("insert into user_data(owner, \"user\", \"key\", data) values ($1, $1, $2, $3) on conflict (owner, \"user\") do update set data = $3")
.await?;
client.execute(&stmt, &[&user, &data]).await?;
client.execute(&stmt, &[&user, &key, &data]).await?;
Ok(())
}
#[instrument(skip(client))]
pub async fn delete_data(
client: &mut impl GenericClient,
user: Uuid,
key: &str,
) -> Result<(), tokio_postgres::Error> {
let stmt = client
.prepare_cached("delete from user_data where \"owner\" = $1 and user = $2")
.prepare_cached(
"delete from user_data where \"owner\" = $1 and \"user\" = $1 and \"key\" = $2",
)
.await?;
client.execute(&stmt, &[&user]).await?;
client.execute(&stmt, &[&user, &key]).await?;
Ok(())
}
}

View File

@ -8,16 +8,60 @@ HTTP 200
[Captures]
token: jsonpath "$.token"
POST {{host}}/api/users/@me/data
GET {{host}}/api/users/@me/data
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$" isCollection
jsonpath "$" isEmpty
GET {{host}}/api/users/@me/data/test
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$" == null
POST {{host}}/api/users/@me/data/test
Authorization: Bearer {{token}}
{
"hello": "world"
}
HTTP 200
GET {{host}}/api/users/@me/data
GET {{host}}/api/users/@me/data/test
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$.hello" == "world"
GET {{host}}/api/users/@me/data
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$" isCollection
jsonpath "$" count == 1
jsonpath "$[0]" == "test"
POST {{host}}/api/users/@me/data/test
Authorization: Bearer {{token}}
null
HTTP 200
GET {{host}}/api/users/@me/data/test
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$" == null
GET {{host}}/api/users/@me/data
Authorization: Bearer {{token}}
HTTP 200
[Asserts]
jsonpath "$" isCollection
jsonpath "$" isEmpty