diff --git a/.sqlx/query-4cd579c6421d05807fb8433d14ea312db0977353e34ef04e2bab31e009151bb2.json b/.sqlx/query-4cd579c6421d05807fb8433d14ea312db0977353e34ef04e2bab31e009151bb2.json deleted file mode 100644 index 2828c373..00000000 --- a/.sqlx/query-4cd579c6421d05807fb8433d14ea312db0977353e34ef04e2bab31e009151bb2.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO af_user (uuid, email)\n SELECT $1, $2\n WHERE NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE email = $2\n )\n AND NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE uuid = $1\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text" - ] - }, - "nullable": [] - }, - "hash": "4cd579c6421d05807fb8433d14ea312db0977353e34ef04e2bab31e009151bb2" -} diff --git a/.sqlx/query-6bbb6f2e06a63df25a7a50624f1931b50c481f29a36b0f9264c1c1d4439f5935.json b/.sqlx/query-6bbb6f2e06a63df25a7a50624f1931b50c481f29a36b0f9264c1c1d4439f5935.json new file mode 100644 index 00000000..57f328db --- /dev/null +++ b/.sqlx/query-6bbb6f2e06a63df25a7a50624f1931b50c481f29a36b0f9264c1c1d4439f5935.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO af_user (uuid, email, name)\n SELECT $1, $2, $3\n WHERE NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE email = $2\n )\n AND NOT EXISTS (\n SELECT 1 FROM public.af_user WHERE uuid = $1\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "6bbb6f2e06a63df25a7a50624f1931b50c481f29a36b0f9264c1c1d4439f5935" +} diff --git a/.sqlx/query-d9003caf83e3c1d85e9a2b732c1d1853e919f5af088d9e670b507599fc0fb331.json b/.sqlx/query-d9003caf83e3c1d85e9a2b732c1d1853e919f5af088d9e670b507599fc0fb331.json new file mode 100644 index 00000000..23424155 --- /dev/null +++ b/.sqlx/query-d9003caf83e3c1d85e9a2b732c1d1853e919f5af088d9e670b507599fc0fb331.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE af_user\n SET name = $1\n WHERE uuid = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "d9003caf83e3c1d85e9a2b732c1d1853e919f5af088d9e670b507599fc0fb331" +} diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 97fbae81..1cdf6539 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -3,6 +3,8 @@ use gotrue_entity::OAuthURL; use reqwest::Method; use reqwest::RequestBuilder; use shared_entity::data::AppResponse; +use shared_entity::dto::SignInParams; +use shared_entity::dto::UserUpdateParams; use shared_entity::dto::WorkspaceMembersParams; use std::time::SystemTime; use storage_entity::AFWorkspaceMember; @@ -205,11 +207,11 @@ impl Client { pub async fn sign_in_password(&mut self, email: &str, password: &str) -> Result<(), AppError> { let url = format!("{}/api/user/sign_in/password", self.base_url); - let payload = serde_json::json!({ - "email": email, - "password": password, - }); - let resp = self.http_client.post(&url).json(&payload).send().await?; + let params = SignInParams { + email: email.to_owned(), + password: password.to_owned(), + }; + let resp = self.http_client.post(&url).json(¶ms).send().await?; self .token .set(AppResponse::from_response(resp).await?.into_data()?); @@ -233,11 +235,11 @@ impl Client { pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), AppError> { let url = format!("{}/api/user/sign_up", self.base_url); - let payload = serde_json::json!({ - "email": email, - "password": password, - }); - let resp = self.http_client.post(&url).json(&payload).send().await?; + let params = SignInParams { + email: email.to_owned(), + password: password.to_owned(), + }; + let resp = self.http_client.post(&url).json(¶ms).send().await?; AppResponse::<()>::from_response(resp).await?.into_error()?; Ok(()) } @@ -254,16 +256,22 @@ impl Client { Ok(()) } - pub async fn update(&mut self, email: &str, password: &str) -> Result<(), AppError> { + pub async fn update( + &mut self, + email: &str, + password: &str, + name: Option<&str>, + ) -> Result<(), AppError> { let url = format!("{}/api/user/update", self.base_url); - let payload = serde_json::json!({ - "email": email, - "password": password, - }); + let params = UserUpdateParams { + email: email.to_owned(), + password: password.to_owned(), + name: name.map(String::from), + }; let resp = self .http_client_with_auth(Method::POST, &url) .await? - .json(&payload) + .json(¶ms) .send() .await?; let new_user = AppResponse::::from_response(resp) diff --git a/libs/shared-entity/src/dto.rs b/libs/shared-entity/src/dto.rs index cea5cf33..f9342e49 100644 --- a/libs/shared-entity/src/dto.rs +++ b/libs/shared-entity/src/dto.rs @@ -5,3 +5,16 @@ pub struct WorkspaceMembersParams { pub workspace_uuid: uuid::Uuid, pub member_emails: Vec, } + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct SignInParams { + pub email: String, + pub password: String, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct UserUpdateParams { + pub email: String, + pub password: String, + pub name: Option, +} diff --git a/libs/storage/src/workspace.rs b/libs/storage/src/workspace.rs index 3254e922..a396a130 100644 --- a/libs/storage/src/workspace.rs +++ b/libs/storage/src/workspace.rs @@ -5,15 +5,35 @@ use sqlx::{ use storage_entity::{AFRole, AFUserProfileView, AFWorkspace, AFWorkspaceMember}; +pub async fn update_user_name( + pool: &PgPool, + gotrue_uuid: &uuid::Uuid, + name: &str, +) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE af_user + SET name = $1 + WHERE uuid = $2 + "#, + name, + gotrue_uuid + ) + .execute(pool) + .await?; + Ok(()) +} + pub async fn create_user_if_not_exists( pool: &PgPool, gotrue_uuid: &uuid::Uuid, email: &str, + name: &str, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO af_user (uuid, email) - SELECT $1, $2 + INSERT INTO af_user (uuid, email, name) + SELECT $1, $2, $3 WHERE NOT EXISTS ( SELECT 1 FROM public.af_user WHERE email = $2 ) @@ -22,7 +42,8 @@ pub async fn create_user_if_not_exists( ) "#, gotrue_uuid, - email + email, + name ) .execute(pool) .await?; diff --git a/src/api/user.rs b/src/api/user.rs index d75aba85..df416ebe 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -9,6 +9,7 @@ use crate::domain::{UserEmail, UserName, UserPassword}; use crate::state::AppState; use gotrue_entity::{AccessTokenResponse, OAuthProvider, OAuthURL, User}; use shared_entity::data::{AppResponse, JsonAppResponse}; +use shared_entity::dto::{SignInParams, UserUpdateParams}; use shared_entity::error::AppError; use shared_entity::error_code::ErrorCode; use storage_entity::AFUserProfileView; @@ -77,12 +78,19 @@ async fn profile_handler( async fn update_handler( auth: Authorization, - req: Json, + req: Json, state: Data, ) -> Result> { let req = req.into_inner(); - let user = - biz::user::update(&state.gotrue_client, &auth.token, &req.email, &req.password).await?; + let user = biz::user::update( + &state.pg_pool, + &state.gotrue_client, + &auth.token, + &req.email, + &req.password, + req.name.as_deref(), + ) + .await?; Ok(AppResponse::Ok().with_data(user).into()) } @@ -100,7 +108,7 @@ async fn sign_out_handler( } async fn sign_in_password_handler( - req: Json, + req: Json, state: Data, ) -> Result> { let req = req.into_inner(); @@ -116,14 +124,15 @@ async fn sign_in_password_handler( } async fn sign_up_handler( - req: Json, + req: Json, state: Data, ) -> Result> { + let req = req.into_inner(); biz::user::sign_up( - &state.gotrue_client, - &req.email, - &req.password, &state.pg_pool, + &state.gotrue_client, + req.email, + req.password, ) .await?; diff --git a/src/biz/user.rs b/src/biz/user.rs index 011d1be6..62e4d713 100644 --- a/src/biz/user.rs +++ b/src/biz/user.rs @@ -28,17 +28,17 @@ pub async fn refresh( #[instrument(level = "info", skip_all, err)] pub async fn sign_up( - gotrue_client: &Client, - email: &str, - password: &str, pg_pool: &PgPool, + gotrue_client: &Client, + email: String, + password: String, ) -> Result<(), AppError> { - validate_email_password(email, password)?; - let user = gotrue_client.sign_up(email, password).await??; + validate_email_password(&email, &password)?; + let user = gotrue_client.sign_up(&email, &password).await??; tracing::info!("user sign up: {:?}", user); if user.confirmed_at.is_some() { let gotrue_uuid = uuid::Uuid::from_str(&user.id)?; - storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &user.email).await?; + storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &user.email, "").await?; } Ok(()) } @@ -50,7 +50,8 @@ pub async fn info( ) -> Result { let user = gotrue_client.user_info(access_token).await??; let user_uuid = uuid::Uuid::from_str(&user.id)?; - storage::workspace::create_user_if_not_exists(pg_pool, &user_uuid, &user.email).await?; + let name: String = name_from_user_metadata(&user.user_metadata); + storage::workspace::create_user_if_not_exists(pg_pool, &user_uuid, &user.email, &name).await?; Ok(user) } @@ -88,20 +89,26 @@ pub async fn sign_in( ) -> Result { let grant = Grant::Password(PasswordGrant { email, password }); let token = gotrue_client.token(&grant).await??; - let gotrue_uuid = uuid::Uuid::from_str(&token.user.id)?; - storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &token.user.email).await?; + storage::workspace::create_user_if_not_exists(pg_pool, &gotrue_uuid, &token.user.email, "") + .await?; Ok(token) } pub async fn update( + pg_pool: &PgPool, gotrue_client: &Client, token: &str, email: &str, password: &str, + name: Option<&str>, ) -> Result { validate_email_password(email, password)?; let user = gotrue_client.update_user(token, email, password).await??; + let user_uuid = user.id.parse::()?; + if let Some(name) = name { + storage::workspace::update_user_name(pg_pool, &user_uuid, name).await?; + } Ok(user) } @@ -114,3 +121,14 @@ fn validate_email_password(email: &str, password: &str) -> Result<(), AppError> Ok(()) } } + +// Best effort to get user's name after oauth +fn name_from_user_metadata(value: &serde_json::Value) -> String { + value + .get("name") + .or(value.get("full_name")) + .or(value.get("nickname")) + .and_then(serde_json::Value::as_str) + .map(str::to_string) + .unwrap_or(String::new()) +} diff --git a/src/component/auth/jwt.rs b/src/component/auth/jwt.rs index 3117ad2f..ae366723 100644 --- a/src/component/auth/jwt.rs +++ b/src/component/auth/jwt.rs @@ -4,9 +4,10 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; -use sqlx::types::uuid; +use sqlx::types::{uuid, Uuid}; use std::fmt::{Display, Formatter}; use std::ops::Deref; +use std::str::FromStr; use crate::state::AppState; @@ -19,21 +20,7 @@ pub struct UserUuid(uuid::Uuid); impl UserUuid { pub fn from_auth(auth: Authorization) -> Result { - let uuid = auth - .claims - .sub - .ok_or(actix_web::error::ErrorUnauthorized( - "Invalid Authorization header, missing sub(uuid)", - )) - .map(|sub| { - uuid::Uuid::parse_str(&sub).map_err(|e| { - actix_web::error::ErrorUnauthorized(format!( - "Invalid Authorization header, invalid sub(uuid): {}", - e - )) - }) - })?; - Ok(Self(uuid?)) + Ok(Self(auth.uuid()?)) } } @@ -83,8 +70,21 @@ pub struct Authorization { } impl Authorization { - pub fn uuid(&self) -> Option { - self.claims.sub.clone() + pub fn uuid(&self) -> Result { + self + .claims + .sub + .as_deref() + .map(Uuid::from_str) + .ok_or(actix_web::error::ErrorUnauthorized( + "Invalid Authorization header, missing sub(uuid)", + ))? + .map_err(|e| { + actix_web::error::ErrorUnauthorized(format!( + "Invalid Authorization header, invalid sub(uuid): {}", + e + )) + }) } } diff --git a/tests/client/sign_in.rs b/tests/client/sign_in.rs index c235791d..eb19eaac 100644 --- a/tests/client/sign_in.rs +++ b/tests/client/sign_in.rs @@ -19,7 +19,6 @@ async fn sign_in_wrong_password() { let email = generate_unique_email(); let password = "Hello123!"; - c.sign_up(&email, password).await.unwrap(); let wrong_password = "Hllo123!"; @@ -37,7 +36,6 @@ async fn sign_in_unconfirmed_email() { let email = generate_unique_email(); let password = "Hello123!"; - c.sign_up(&email, password).await.unwrap(); let err = c.sign_in_password(&email, password).await.unwrap_err(); diff --git a/tests/client/sign_up.rs b/tests/client/sign_up.rs index 95465f14..e0461cc2 100644 --- a/tests/client/sign_up.rs +++ b/tests/client/sign_up.rs @@ -18,8 +18,10 @@ async fn sign_up_success() { async fn sign_up_invalid_email() { let invalid_email = "not_email_address"; let password = "Hello!123#"; - let c = client_api_client(); - let error = c.sign_up(invalid_email, password).await.unwrap_err(); + let error = client_api_client() + .sign_up(invalid_email, password) + .await + .unwrap_err(); assert_eq!(error.code, ErrorCode::InvalidEmail); assert_eq!(error.message, "invalid email: not_email_address"); } diff --git a/tests/client/update.rs b/tests/client/update.rs index 709fa9b1..5e0af211 100644 --- a/tests/client/update.rs +++ b/tests/client/update.rs @@ -8,7 +8,7 @@ async fn update_but_not_logged_in() { let mut c = client_api_client(); let new_email = generate_unique_email(); let new_password = "Hello123!"; - let res = c.update(&new_email, new_password).await; + let res = c.update(&new_email, new_password, None).await; assert!(res.is_err()); } @@ -21,7 +21,11 @@ async fn update_password_same_password() { c.sign_in_password(&user.email, &user.password) .await .unwrap(); - let err = c.update(&user.email, &user.password).await.err().unwrap(); + let err = c + .update(&user.email, &user.password, None) + .await + .err() + .unwrap(); assert_eq!(err.code, ErrorCode::InvalidPassword); assert_eq!( err.message, @@ -41,12 +45,12 @@ async fn update_password_and_revert() { c.sign_in_password(&user.email, &user.password) .await .unwrap(); - c.update(&user.email, new_password).await.unwrap(); + c.update(&user.email, new_password, None).await.unwrap(); } { // revert password to old_password let mut c = client_api_client(); c.sign_in_password(&user.email, new_password).await.unwrap(); - c.update(&user.email, &user.password).await.unwrap(); + c.update(&user.email, &user.password, None).await.unwrap(); } }