feat: insert user metadata (#131)
This commit is contained in:
parent
1aba1f0cf4
commit
7a309c6f69
|
|
@ -30,26 +30,31 @@
|
|||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "encryption_sign",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "deleted_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "latest_workspace_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
|
|
@ -69,6 +74,7 @@
|
|||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ pub struct AFUserProfileView {
|
|||
pub email: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub encryption_sign: Option<String>,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,32 @@
|
|||
use anyhow::Context;
|
||||
use database_entity::database_error::DatabaseError;
|
||||
use sqlx::postgres::PgArguments;
|
||||
use sqlx::types::JsonValue;
|
||||
use sqlx::{Arguments, Executor, PgPool, Postgres};
|
||||
use tracing::{instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Updates the user's details in the `af_user` table.
|
||||
///
|
||||
/// This function allows for updating the user's name, email, and metadata based on the provided UUID.
|
||||
/// If the `metadata` is provided, it merges the new metadata with the existing one, with the new values
|
||||
/// overriding the old ones in case of conflicts.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pool` - A reference to the database connection pool.
|
||||
/// * `user_uuid` - The UUID of the user to be updated.
|
||||
/// * `name` - An optional new name for the user.
|
||||
/// * `email` - An optional new email for the user.
|
||||
/// * `metadata` - An optional JSON value containing new metadata for the user.
|
||||
///
|
||||
#[instrument(skip_all, err)]
|
||||
pub async fn update_user(
|
||||
pool: &PgPool,
|
||||
user_uuid: &uuid::Uuid,
|
||||
name: Option<String>,
|
||||
email: Option<String>,
|
||||
metadata: Option<JsonValue>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut set_clauses = Vec::new();
|
||||
let mut args = PgArguments::default();
|
||||
|
|
@ -28,6 +44,13 @@ pub async fn update_user(
|
|||
args.add(e);
|
||||
}
|
||||
|
||||
if let Some(m) = metadata {
|
||||
args_num += 1;
|
||||
// Merge existing metadata with new metadata
|
||||
set_clauses.push(format!("metadata = metadata || ${}", args_num));
|
||||
args.add(m);
|
||||
}
|
||||
|
||||
if set_clauses.is_empty() {
|
||||
warn!("No update params provided");
|
||||
return Ok(());
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ gotrue-entity = { path = "../gotrue-entity" }
|
|||
database-entity = { path = "../database-entity" }
|
||||
|
||||
actix-web = { version = "4.4.0", default-features = false, features = ["http2"], optional = true }
|
||||
sqlx = { version = "0.7", default-features = false, features = ["postgres"], optional = true }
|
||||
sqlx = { version = "0.7", default-features = false, features = ["postgres", "json"], optional = true }
|
||||
validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true }
|
||||
opener = "0.6.1"
|
||||
url = "2.4.1"
|
||||
|
|
|
|||
|
|
@ -1,39 +1,58 @@
|
|||
// Data Transfer Objects (DTO)
|
||||
|
||||
use gotrue_entity::AccessTokenResponse;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct SignInParams {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone)]
|
||||
pub struct UserMetaData(HashMap<String, serde_json::Value>);
|
||||
impl UserMetaData {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn into_inner(self) -> HashMap<String, serde_json::Value> {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn insert<T: Into<serde_json::Value>>(&mut self, key: &str, value: T) {
|
||||
self.0.insert(key.to_string(), value.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Default)]
|
||||
pub struct UpdateUsernameParams {
|
||||
pub name: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub metadata: Option<UserMetaData>,
|
||||
}
|
||||
|
||||
impl UpdateUsernameParams {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_password<T: ToString>(mut self, password: T) -> Self {
|
||||
self.password = Some(password.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_name<T: ToString>(mut self, name: T) -> Self {
|
||||
self.name = Some(name.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_email<T: ToString>(mut self, email: T) -> Self {
|
||||
self.email = Some(email.to_string());
|
||||
self
|
||||
}
|
||||
pub fn with_metadata<T: Into<UserMetaData>>(mut self, metadata: T) -> Self {
|
||||
self.metadata = Some(metadata.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS af_user (
|
|||
email TEXT NOT NULL DEFAULT '' UNIQUE, -- not needed when authenticated with gotrue
|
||||
password TEXT NOT NULL DEFAULT '', -- not needed when authenticated with gotrue
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
metadata JSONB DEFAULT '{}'::JSONB, -- used to user's metadata such as avatar, OpenAI key, etc.
|
||||
encryption_sign TEXT DEFAULT NULL, -- used to encrypt the user's data
|
||||
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ use tracing_actix_web::RequestId;
|
|||
pub fn user_scope() -> Scope {
|
||||
web::scope("/api/user")
|
||||
// auth server integration
|
||||
.service(web::resource("/verify/{access_token}").route(web::get().to(verify_handler)))
|
||||
.service(web::resource("/update").route(web::post().to(update_handler)))
|
||||
.service(web::resource("/profile").route(web::get().to(profile_handler)))
|
||||
.service(web::resource("/verify/{access_token}").route(web::get().to(verify_user_handler)))
|
||||
.service(web::resource("/update").route(web::post().to(update_user_handler)))
|
||||
.service(web::resource("/profile").route(web::get().to(get_user_profile_handler)))
|
||||
|
||||
// native
|
||||
// deprecated
|
||||
.service(web::resource("/login").route(web::post().to(login_handler)))
|
||||
.service(web::resource("/logout").route(web::get().to(logout_handler)))
|
||||
.service(web::resource("/register").route(web::post().to(register_handler)))
|
||||
|
|
@ -35,7 +35,7 @@ pub fn user_scope() -> Scope {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip(state, path), err)]
|
||||
async fn verify_handler(
|
||||
async fn verify_user_handler(
|
||||
path: web::Path<String>,
|
||||
state: Data<AppState>,
|
||||
required_id: RequestId,
|
||||
|
|
@ -47,7 +47,7 @@ async fn verify_handler(
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip(state), err)]
|
||||
async fn profile_handler(
|
||||
async fn get_user_profile_handler(
|
||||
uuid: UserUuid,
|
||||
state: Data<AppState>,
|
||||
required_id: RequestId,
|
||||
|
|
@ -57,7 +57,7 @@ async fn profile_handler(
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip(state, auth, payload), err)]
|
||||
async fn update_handler(
|
||||
async fn update_user_handler(
|
||||
auth: Authorization,
|
||||
payload: Json<UpdateUsernameParams>,
|
||||
state: Data<AppState>,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use database::{user::create_user_if_not_exists, workspace::select_user_profile_view_by_uuid};
|
||||
use database_entity::AFUserProfileView;
|
||||
use gotrue::api::Client;
|
||||
use serde_json::json;
|
||||
use shared_entity::app_error::AppError;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -35,7 +36,8 @@ pub async fn update_user(
|
|||
user_uuid: Uuid,
|
||||
params: UpdateUsernameParams,
|
||||
) -> Result<(), AppError> {
|
||||
Ok(database::user::update_user(pg_pool, &user_uuid, params.name, params.email).await?)
|
||||
let metadata = params.metadata.map(|m| json!(m.into_inner()));
|
||||
Ok(database::user::update_user(pg_pool, &user_uuid, params.name, params.email, metadata).await?)
|
||||
}
|
||||
|
||||
// Best effort to get user's name after oauth
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use shared_entity::dto::auth_dto::UpdateUsernameParams;
|
||||
use serde_json::json;
|
||||
use shared_entity::dto::auth_dto::{UpdateUsernameParams, UserMetaData};
|
||||
use shared_entity::error_code::ErrorCode;
|
||||
|
||||
use crate::localhost_client;
|
||||
|
|
@ -73,3 +74,69 @@ async fn update_user_name() {
|
|||
let profile = c.get_profile().await.unwrap();
|
||||
assert_eq!(profile.name.unwrap().as_str(), "lucas");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_user_metadata() {
|
||||
let (c, user) = generate_unique_registered_user_client().await;
|
||||
c.sign_in_password(&user.email, &user.password)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut metadata = UserMetaData::new();
|
||||
metadata.insert("str_value", "value");
|
||||
metadata.insert("int_value", 1);
|
||||
|
||||
c.update_user(UpdateUsernameParams::new().with_metadata(metadata.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let profile = c.get_profile().await.unwrap();
|
||||
assert_eq!(profile.metadata.unwrap(), json!(metadata));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_metadata_override() {
|
||||
let (c, user) = generate_unique_registered_user_client().await;
|
||||
c.sign_in_password(&user.email, &user.password)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut metadata_1 = UserMetaData::new();
|
||||
metadata_1.insert("str_value", "value");
|
||||
metadata_1.insert("int_value", 1);
|
||||
c.update_user(UpdateUsernameParams::new().with_metadata(metadata_1.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut metadata_2 = UserMetaData::new();
|
||||
metadata_2.insert("bool_value", false);
|
||||
c.update_user(UpdateUsernameParams::new().with_metadata(metadata_2))
|
||||
.await
|
||||
.unwrap();
|
||||
metadata_1.insert("bool_value", false);
|
||||
|
||||
let profile = c.get_profile().await.unwrap();
|
||||
assert_eq!(profile.metadata.unwrap(), json!(metadata_1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_empty_metadata_override() {
|
||||
let (c, user) = generate_unique_registered_user_client().await;
|
||||
c.sign_in_password(&user.email, &user.password)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut metadata_1 = UserMetaData::new();
|
||||
metadata_1.insert("str_value", "value");
|
||||
metadata_1.insert("int_value", 1);
|
||||
c.update_user(UpdateUsernameParams::new().with_metadata(metadata_1.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
c.update_user(UpdateUsernameParams::new().with_metadata(UserMetaData::new()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let profile = c.get_profile().await.unwrap();
|
||||
assert_eq!(profile.metadata.unwrap(), json!(metadata_1));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue