Skip to content

Commit

Permalink
feature: support get users API
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrchen committed Apr 29, 2024
1 parent 7b753fb commit c3d4404
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 19 deletions.
6 changes: 3 additions & 3 deletions chat_server/src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions chat_server/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod auth;
mod chat;
mod messages;
mod workspace;

use axum::response::IntoResponse;

pub(crate) use auth::*;
pub(crate) use chat::*;
pub(crate) use messages::*;
pub(crate) use workspace::*;

pub(crate) async fn index_handler() -> impl IntoResponse {
"index"
Expand Down
11 changes: 11 additions & 0 deletions chat_server/src/handlers/workspace.rs
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))
}
1 change: 1 addition & 0 deletions chat_server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub async fn get_router(config: AppConfig) -> Result<Router, AppError> {
let state = AppState::try_new(config).await?;

let api = Router::new()
.route("/users", get(list_chat_users_handler))
.route("/chat", get(list_chat_handler).post(create_chat_handler))
.route(
"/chat/:id",
Expand Down
17 changes: 17 additions & 0 deletions chat_server/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod user;
mod workspace;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
Expand All @@ -9,10 +10,26 @@ pub use user::{CreateUser, SigninUser};
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, PartialEq)]
pub struct User {
pub id: i64,
pub ws_id: i64,
pub fullname: String,
pub email: String,
#[sqlx(default)]
#[serde(skip)]
pub password_hash: Option<String>,
pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, FromRow, Serialize, Deserialize, PartialEq)]
pub struct Workspace {
pub id: i64,
pub name: String,
pub owner_id: i64,
pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, FromRow, Serialize, Deserialize, PartialEq)]
pub struct ChatUser {
pub id: i64,
pub fullname: String,
pub email: String,
}
52 changes: 38 additions & 14 deletions chat_server/src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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)
Expand All @@ -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);

Expand Down Expand Up @@ -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,
Expand All @@ -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(),
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
156 changes: 156 additions & 0 deletions chat_server/src/models/workspace.rs
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(())
}
}
Loading

0 comments on commit c3d4404

Please sign in to comment.