-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
266 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,7 +49,7 @@ mod tests { | |
async fn signup_should_work() -> Result<()> { | ||
let config = AppConfig::load()?; | ||
let (_tdb, state) = AppState::new_for_test(config).await?; | ||
let input = CreateUser::new("Tyr Chen", "[email protected]", "Hunter42"); | ||
let input = CreateUser::new("none", "Tyr Chen", "[email protected]", "Hunter42"); | ||
let ret = signup_handler(State(state), Json(input)) | ||
.await? | ||
.into_response(); | ||
|
@@ -64,7 +64,7 @@ mod tests { | |
async fn signup_duplicate_user_should_409() -> Result<()> { | ||
let config = AppConfig::load()?; | ||
let (_tdb, state) = AppState::new_for_test(config).await?; | ||
let input = CreateUser::new("Tyr Chen", "[email protected]", "Hunter42"); | ||
let input = CreateUser::new("none", "Tyr Chen", "[email protected]", "Hunter42"); | ||
signup_handler(State(state.clone()), Json(input.clone())).await?; | ||
let ret = signup_handler(State(state.clone()), Json(input.clone())) | ||
.await | ||
|
@@ -84,7 +84,7 @@ mod tests { | |
let name = "Alice"; | ||
let email = "[email protected]"; | ||
let password = "Hunter42"; | ||
let user = CreateUser::new(name, email, password); | ||
let user = CreateUser::new("none", name, email, password); | ||
User::create(&user, &state.pool).await?; | ||
let input = SigninUser::new(email, password); | ||
let ret = signin_handler(State(state), Json(input)) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
use axum::{extract::State, response::IntoResponse, Extension, Json}; | ||
|
||
use crate::{models::Workspace, AppError, AppState, User}; | ||
|
||
pub(crate) async fn list_chat_users_handler( | ||
Extension(user): Extension<User>, | ||
State(state): State<AppState>, | ||
) -> Result<impl IntoResponse, AppError> { | ||
let users = Workspace::fetch_all_chat_users(user.ws_id as _, &state.pool).await?; | ||
Ok(Json(users)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,10 +7,13 @@ use serde::{Deserialize, Serialize}; | |
use sqlx::PgPool; | ||
use std::mem; | ||
|
||
use super::{ChatUser, Workspace}; | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)] | ||
pub struct CreateUser { | ||
pub fullname: String, | ||
pub email: String, | ||
pub workspace: String, | ||
pub password: String, | ||
} | ||
|
||
|
@@ -23,41 +26,56 @@ pub struct SigninUser { | |
impl User { | ||
/// Find a user by email | ||
pub async fn find_by_email(email: &str, pool: &PgPool) -> Result<Option<Self>, AppError> { | ||
let user = | ||
sqlx::query_as("SELECT id, fullname, email, created_at FROM users WHERE email = $1") | ||
.bind(email) | ||
.fetch_optional(pool) | ||
.await?; | ||
let user = sqlx::query_as( | ||
"SELECT id, ws_id, fullname, email, created_at FROM users WHERE email = $1", | ||
) | ||
.bind(email) | ||
.fetch_optional(pool) | ||
.await?; | ||
Ok(user) | ||
} | ||
|
||
/// Create a new user | ||
// TODO: use transaction for workspace creation and user creation | ||
pub async fn create(input: &CreateUser, pool: &PgPool) -> Result<Self, AppError> { | ||
let password_hash = hash_password(&input.password)?; | ||
// check if email exists | ||
let user = Self::find_by_email(&input.email, pool).await?; | ||
if user.is_some() { | ||
return Err(AppError::EmailAlreadyExists(input.email.clone())); | ||
} | ||
let user = sqlx::query_as( | ||
|
||
// check if workspace exists, if not create one | ||
let ws = match Workspace::find_by_name(&input.workspace, pool).await? { | ||
Some(ws) => ws, | ||
None => Workspace::create(&input.workspace, 0, pool).await?, | ||
}; | ||
|
||
let password_hash = hash_password(&input.password)?; | ||
let user: User = sqlx::query_as( | ||
r#" | ||
INSERT INTO users (email, fullname, password_hash) | ||
VALUES ($1, $2, $3) | ||
RETURNING id, fullname, email, created_at | ||
INSERT INTO users (ws_id, email, fullname, password_hash) | ||
VALUES ($1, $2, $3, $4) | ||
RETURNING id, ws_id, fullname, email, created_at | ||
"#, | ||
) | ||
.bind(ws.id) | ||
.bind(&input.email) | ||
.bind(&input.fullname) | ||
.bind(password_hash) | ||
.fetch_one(pool) | ||
.await?; | ||
|
||
if ws.owner_id == 0 { | ||
ws.update_owner(user.id as _, pool).await?; | ||
} | ||
|
||
Ok(user) | ||
} | ||
|
||
/// Verify email and password | ||
pub async fn verify(input: &SigninUser, pool: &PgPool) -> Result<Option<Self>, AppError> { | ||
let user: Option<User> = sqlx::query_as( | ||
"SELECT id, fullname, email, password_hash, created_at FROM users WHERE email = $1", | ||
"SELECT id, ws_id, fullname, email, password_hash, created_at FROM users WHERE email = $1", | ||
) | ||
.bind(&input.email) | ||
.fetch_optional(pool) | ||
|
@@ -78,6 +96,10 @@ impl User { | |
} | ||
} | ||
|
||
impl ChatUser { | ||
// pub async fn fetch_all(user: &User) | ||
} | ||
|
||
fn hash_password(password: &str) -> Result<String, AppError> { | ||
let salt = SaltString::generate(&mut OsRng); | ||
|
||
|
@@ -109,6 +131,7 @@ impl User { | |
pub fn new(id: i64, fullname: &str, email: &str) -> Self { | ||
Self { | ||
id, | ||
ws_id: 0, | ||
fullname: fullname.to_string(), | ||
email: email.to_string(), | ||
password_hash: None, | ||
|
@@ -119,9 +142,10 @@ impl User { | |
|
||
#[cfg(test)] | ||
impl CreateUser { | ||
pub fn new(fullname: &str, email: &str, password: &str) -> Self { | ||
pub fn new(ws: &str, fullname: &str, email: &str, password: &str) -> Self { | ||
Self { | ||
fullname: fullname.to_string(), | ||
workspace: ws.to_string(), | ||
email: email.to_string(), | ||
password: password.to_string(), | ||
} | ||
|
@@ -162,7 +186,7 @@ mod tests { | |
); | ||
let pool = tdb.get_pool().await; | ||
|
||
let input = CreateUser::new("Tyr Chen", "[email protected]", "hunter42"); | ||
let input = CreateUser::new("none", "Tyr Chen", "[email protected]", "hunter42"); | ||
User::create(&input, &pool).await?; | ||
let ret = User::create(&input, &pool).await; | ||
match ret { | ||
|
@@ -182,7 +206,7 @@ mod tests { | |
); | ||
let pool = tdb.get_pool().await; | ||
|
||
let input = CreateUser::new("Tyr Chen", "[email protected]", "hunter42"); | ||
let input = CreateUser::new("none", "Tyr Chen", "[email protected]", "hunter42"); | ||
let user = User::create(&input, &pool).await?; | ||
assert_eq!(user.email, input.email); | ||
assert_eq!(user.fullname, input.fullname); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
use sqlx::PgPool; | ||
|
||
use crate::AppError; | ||
|
||
use super::{ChatUser, Workspace}; | ||
|
||
impl Workspace { | ||
pub async fn create(name: &str, user_id: u64, pool: &PgPool) -> Result<Self, AppError> { | ||
let ws = sqlx::query_as( | ||
r#" | ||
INSERT INTO workspaces (name, owner_id) | ||
VALUES ($1, $2) | ||
RETURNING id, name, owner_id, created_at | ||
"#, | ||
) | ||
.bind(name) | ||
.bind(user_id as i64) | ||
.fetch_one(pool) | ||
.await?; | ||
|
||
Ok(ws) | ||
} | ||
|
||
pub async fn update_owner(&self, owner_id: u64, pool: &PgPool) -> Result<Self, AppError> { | ||
// update owner_id in two cases 1) owner_id = 0 2) owner's ws_id = id | ||
let ws = sqlx::query_as( | ||
r#" | ||
UPDATE workspaces | ||
SET owner_id = $1 | ||
WHERE id = $2 and (SELECT ws_id FROM users WHERE id = $1) = $2 | ||
RETURNING id, name, owner_id, created_at | ||
"#, | ||
) | ||
.bind(owner_id as i64) | ||
.bind(self.id) | ||
.fetch_one(pool) | ||
.await?; | ||
|
||
Ok(ws) | ||
} | ||
|
||
pub async fn find_by_name(name: &str, pool: &PgPool) -> Result<Option<Self>, AppError> { | ||
let ws = sqlx::query_as( | ||
r#" | ||
SELECT id, name, owner_id, created_at | ||
FROM workspaces | ||
WHERE name = $1 | ||
"#, | ||
) | ||
.bind(name) | ||
.fetch_optional(pool) | ||
.await?; | ||
|
||
Ok(ws) | ||
} | ||
|
||
#[allow(dead_code)] | ||
pub async fn find_by_id(id: u64, pool: &PgPool) -> Result<Option<Self>, AppError> { | ||
let ws = sqlx::query_as( | ||
r#" | ||
SELECT id, name, owner_id, created_at | ||
FROM workspaces | ||
WHERE id = $1 | ||
"#, | ||
) | ||
.bind(id as i64) | ||
.fetch_optional(pool) | ||
.await?; | ||
|
||
Ok(ws) | ||
} | ||
|
||
#[allow(dead_code)] | ||
pub async fn fetch_all_chat_users(id: u64, pool: &PgPool) -> Result<Vec<ChatUser>, AppError> { | ||
let users = sqlx::query_as( | ||
r#" | ||
SELECT id, fullname, email | ||
FROM users | ||
WHERE ws_id = $1 order by id | ||
"#, | ||
) | ||
.bind(id as i64) | ||
.fetch_all(pool) | ||
.await?; | ||
|
||
Ok(users) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::path::Path; | ||
|
||
use crate::{models::CreateUser, User}; | ||
|
||
use super::*; | ||
use anyhow::{Ok, Result}; | ||
use sqlx_db_tester::TestPg; | ||
|
||
#[tokio::test] | ||
async fn workspace_should_create_and_set_owner() -> Result<()> { | ||
let tdb = TestPg::new( | ||
"postgres://postgres:postgres@localhost:5432".to_string(), | ||
Path::new("../migrations"), | ||
); | ||
let pool = tdb.get_pool().await; | ||
let ws = Workspace::create("test", 0, &pool).await.unwrap(); | ||
|
||
let input = CreateUser::new(&ws.name, "Tyr Chen", "[email protected]", "Hunter42"); | ||
let user = User::create(&input, &pool).await.unwrap(); | ||
|
||
assert_eq!(ws.name, "test"); | ||
|
||
assert_eq!(user.ws_id, ws.id); | ||
|
||
let ws = ws.update_owner(user.id as _, &pool).await.unwrap(); | ||
|
||
assert_eq!(ws.owner_id, user.id); | ||
Ok(()) | ||
} | ||
|
||
#[tokio::test] | ||
async fn workspace_should_find_by_name() -> Result<()> { | ||
let tdb = TestPg::new( | ||
"postgres://postgres:postgres@localhost:5432".to_string(), | ||
Path::new("../migrations"), | ||
); | ||
let pool = tdb.get_pool().await; | ||
let _ws = Workspace::create("test", 0, &pool).await?; | ||
let ws = Workspace::find_by_name("test", &pool).await?; | ||
|
||
assert_eq!(ws.unwrap().name, "test"); | ||
Ok(()) | ||
} | ||
|
||
#[tokio::test] | ||
async fn workspace_should_fetch_all_chat_users() -> Result<()> { | ||
let tdb = TestPg::new( | ||
"postgres://postgres:postgres@localhost:5432".to_string(), | ||
Path::new("../migrations"), | ||
); | ||
let pool = tdb.get_pool().await; | ||
let ws = Workspace::create("test", 0, &pool).await?; | ||
let input = CreateUser::new(&ws.name, "Tyr Chen", "[email protected]", "Hunter42"); | ||
let user1 = User::create(&input, &pool).await?; | ||
let input = CreateUser::new(&ws.name, "Alice Wang", "[email protected]", "Hunter42"); | ||
let user2 = User::create(&input, &pool).await?; | ||
|
||
let users = Workspace::fetch_all_chat_users(ws.id as _, &pool).await?; | ||
assert_eq!(users.len(), 2); | ||
assert_eq!(users[0].id, user1.id); | ||
assert_eq!(users[1].id, user2.id); | ||
|
||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.