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( create table user_data(
owner uuid not null references users(id) on delete cascade, owner uuid not null references users(id) on delete cascade,
"user" 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, 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() Router::new()
.route("/{target}", get(user_info)) .route("/{target}", get(user_info))
.route("/@me/balance", get(user_balance)) .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/accounts", get(user_accounts))
.route("/@me/transactions", get(me_transaction_history)) .route("/@me/transactions", get(me_transaction_history))
.route("/", get(list_users)) .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 })) 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))] #[instrument(skip(state))]
pub async fn get_user_data( pub async fn get_user_data(
EState(state): State, EState(state): State,
auth: Auth, auth: Auth,
Path(key): Path<String>,
) -> Result<Json<Option<serde_json::Value>>, Error> { ) -> Result<Json<Option<serde_json::Value>>, Error> {
let user = auth.user_id(); let user = auth.user_id();
let conn = state.conn().await?; 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)) Ok(Json(data))
} }
@ -116,13 +129,14 @@ pub async fn get_user_data(
pub async fn set_user_data( pub async fn set_user_data(
EState(state): State, EState(state): State,
auth: Auth, auth: Auth,
Path(key): Path<String>,
Json(body): Json<Option<serde_json::Value>>, Json(body): Json<Option<serde_json::Value>>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let user = auth.user_id(); let user = auth.user_id();
let mut conn = state.conn().await?; let mut conn = state.conn().await?;
match body { match body {
Some(data) => Users::set_data(&mut conn, user, data).await?, Some(data) => Users::set_data(&mut conn, user, &key, data).await?,
None => Users::delete_data(&mut conn, user).await?, None => Users::delete_data(&mut conn, user, &key).await?,
} }
Ok(()) Ok(())
} }

View File

@ -121,37 +121,64 @@ impl Users {
Ok(info) 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( pub async fn get_data(
client: &impl GenericClient, client: &impl GenericClient,
user: Uuid, user: Uuid,
key: &str,
) -> Result<Option<serde_json::Value>, tokio_postgres::Error> { ) -> Result<Option<serde_json::Value>, tokio_postgres::Error> {
let stmt = client 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?; .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))) Ok(res.map(|row| row.get(0)))
} }
#[instrument(skip(client, data))]
pub async fn set_data( pub async fn set_data(
client: &mut impl GenericClient, client: &mut impl GenericClient,
user: Uuid, user: Uuid,
key: &str,
data: serde_json::Value, data: serde_json::Value,
) -> Result<(), tokio_postgres::Error> { ) -> Result<(), tokio_postgres::Error> {
let stmt = client 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?; .await?;
client.execute(&stmt, &[&user, &data]).await?; client.execute(&stmt, &[&user, &key, &data]).await?;
Ok(()) Ok(())
} }
#[instrument(skip(client))]
pub async fn delete_data( pub async fn delete_data(
client: &mut impl GenericClient, client: &mut impl GenericClient,
user: Uuid, user: Uuid,
key: &str,
) -> Result<(), tokio_postgres::Error> { ) -> Result<(), tokio_postgres::Error> {
let stmt = client 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?; .await?;
client.execute(&stmt, &[&user]).await?; client.execute(&stmt, &[&user, &key]).await?;
Ok(()) Ok(())
} }
} }

View File

@ -8,16 +8,60 @@ HTTP 200
[Captures] [Captures]
token: jsonpath "$.token" 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}} Authorization: Bearer {{token}}
{ {
"hello": "world" "hello": "world"
} }
HTTP 200 HTTP 200
GET {{host}}/api/users/@me/data GET {{host}}/api/users/@me/data/test
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
HTTP 200 HTTP 200
[Asserts] [Asserts]
jsonpath "$.hello" == "world" 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