feat: insert user metadata (#131)

This commit is contained in:
Nathan.fooo 2023-10-22 19:21:23 +08:00 committed by GitHub
parent 1aba1f0cf4
commit 7a309c6f69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 18 deletions

View File

@ -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
]
},

View File

@ -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>>,

View File

@ -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(());

View File

@ -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"

View File

@ -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)]

View File

@ -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,

View File

@ -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>,

View File

@ -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

View File

@ -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));
}