Skip to content

Commit

Permalink
Merge pull request #1 from tyr-rust-bootcamp/feature/v10-11-load-chats
Browse files Browse the repository at this point in the history
feature: improve chat api to make sure no security hole
  • Loading branch information
tyrchen authored Aug 18, 2024
2 parents cf7f073 + 2fad8fc commit bb202b0
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 14 deletions.
6 changes: 4 additions & 2 deletions chat_server/src/handlers/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub(crate) async fn list_chat_handler(
Extension(user): Extension<User>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, AppError> {
let chat = state.fetch_chats(user.ws_id as _).await?;
let chat = state.fetch_chats(user.id as _, user.ws_id as _).await?;
Ok((StatusCode::OK, Json(chat)))
}

Expand All @@ -40,7 +40,9 @@ pub(crate) async fn create_chat_handler(
State(state): State<AppState>,
Json(input): Json<CreateChat>,
) -> Result<impl IntoResponse, AppError> {
let chat = state.create_chat(input, user.ws_id as _).await?;
let chat = state
.create_chat(input, user.id as _, user.ws_id as _)
.await?;
Ok((StatusCode::CREATED, Json(chat)))
}

Expand Down
10 changes: 10 additions & 0 deletions chat_server/src/handlers/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ use crate::{AppError, AppState};
use axum::{extract::State, response::IntoResponse, Extension, Json};
use chat_core::User;

#[utoipa::path(
get,
path = "/api/users",
responses(
(status = 200, description = "List of ws users", body = Vec<ChatUser>),
),
security(
("token" = [])
)
)]
pub(crate) async fn list_chat_users_handler(
Extension(user): Extension<User>,
State(state): State<AppState>,
Expand Down
38 changes: 31 additions & 7 deletions chat_server/src/models/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,34 @@ pub struct CreateChat {

#[allow(dead_code)]
impl AppState {
pub async fn create_chat(&self, input: CreateChat, ws_id: u64) -> Result<Chat, AppError> {
pub async fn create_chat(
&self,
input: CreateChat,
user_id: u64,
ws_id: u64,
) -> Result<Chat, AppError> {
let len = input.members.len();
if len < 2 {
return Err(AppError::CreateChatError(
"Chat must have at least 2 members".to_string(),
));
}

// if user id is not in members, reject
if !input.members.contains(&(user_id as i64)) {
return Err(AppError::CreateChatError(
"You must be a member of the chat".to_string(),
));
}

if let Some(name) = &input.name {
if name.len() < 3 {
return Err(AppError::CreateChatError(
"Chat name must have at least 3 characters".to_string(),
));
}
}

if len > 8 && input.name.is_none() {
return Err(AppError::CreateChatError(
"Group chat with more than 8 members must have a name".to_string(),
Expand Down Expand Up @@ -62,15 +82,16 @@ impl AppState {
Ok(chat)
}

pub async fn fetch_chats(&self, ws_id: u64) -> Result<Vec<Chat>, AppError> {
pub async fn fetch_chats(&self, user_id: u64, ws_id: u64) -> Result<Vec<Chat>, AppError> {
let chats = sqlx::query_as(
r#"
SELECT id, ws_id, name, type, members, created_at
FROM chats
WHERE ws_id = $1
WHERE ws_id = $1 AND $2 = ANY(members)
"#,
)
.bind(ws_id as i64)
.bind(user_id as i64)
.fetch_all(&self.pool)
.await?;

Expand Down Expand Up @@ -135,7 +156,7 @@ mod tests {
let (_tdb, state) = AppState::new_for_test().await?;
let input = CreateChat::new("", &[1, 2], false);
let chat = state
.create_chat(input, 1)
.create_chat(input, 1, 1)
.await
.expect("create chat failed");
assert_eq!(chat.ws_id, 1);
Expand All @@ -147,9 +168,9 @@ mod tests {
#[tokio::test]
async fn create_public_named_chat_should_work() -> Result<()> {
let (_tdb, state) = AppState::new_for_test().await?;
let input = CreateChat::new("general", &[1, 2, 3], true);
let input = CreateChat::new("general1", &[1, 2, 3], true);
let chat = state
.create_chat(input, 1)
.create_chat(input, 1, 1)
.await
.expect("create chat failed");
assert_eq!(chat.ws_id, 1);
Expand Down Expand Up @@ -178,7 +199,10 @@ mod tests {
#[tokio::test]
async fn chat_fetch_all_should_work() -> Result<()> {
let (_tdb, state) = AppState::new_for_test().await?;
let chats = state.fetch_chats(1).await.expect("fetch all chats failed");
let chats = state
.fetch_chats(1, 1)
.await
.expect("fetch all chats failed");

assert_eq!(chats.len(), 4);

Expand Down
1 change: 1 addition & 0 deletions chat_server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) trait OpenApiRouter {
create_chat_handler,
get_chat_handler,
list_message_handler,
list_chat_users_handler,
),
components(
schemas(User, Chat, ChatType, ChatUser, Message, Workspace, SigninUser, CreateUser, CreateChat, CreateMessage, ListMessages, AuthOutput, ErrorOutput),
Expand Down
6 changes: 5 additions & 1 deletion migrations/20240426045903_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ CREATE TABLE IF NOT EXISTS chats(
type chat_type NOT NULL,
-- user id list
members bigint[] NOT NULL,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP
created_at timestamptz DEFAULT CURRENT_TIMESTAMP,
UNIQUE (ws_id, name, members)
);

-- create message table
Expand All @@ -66,3 +67,6 @@ CREATE INDEX IF NOT EXISTS chat_id_created_at_index ON messages(chat_id, created

-- create index for messages for sender_id
CREATE INDEX IF NOT EXISTS sender_id_index ON messages(sender_id, created_at DESC);

-- create index for chat members
CREATE INDEX IF NOT EXISTS chat_members_index ON chats USING GIN(members);
64 changes: 60 additions & 4 deletions test.rest
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,30 @@ Content-Type: application/json
"password": "123456"
}

### signin user (valid)
### signup user

POST http://localhost:6688/api/signin
POST http://localhost:6688/api/signup
Content-Type: application/json

{
"email": "[email protected]",
"workspace": "acme",
"fullname": "Bob Hua",
"email": "[email protected]",
"password": "123456"
}

### signin user (invalid)

POST http://localhost:6688/api/signin
Content-Type: application/json

{
"email": "[email protected]",
"password": "a123456"
}

### signin user (valid)

# @name signin
POST http://localhost:6688/api/signin
Content-Type: application/json
Expand All @@ -45,17 +57,61 @@ Content-Type: application/json

@token = {{signin.response.body.token}}

### signin user (valid)

# @name signin1
POST http://localhost:6688/api/signin
Content-Type: application/json

{
"email": "[email protected]",
"password": "123456"
}

@token1 = {{signin1.response.body.token}}

### create chat
POST http://localhost:6688/api/chats
Content-Type: application/json
Authorization: Bearer {{token}}

{
"name": "acme",
"name": "project X",
"members": [1, 2],
"public": false
}

### create direct chat
POST http://localhost:6688/api/chats
Content-Type: application/json
Authorization: Bearer {{token}}

{
"members": [1, 2],
"public": false
}

### create chats without me
POST http://localhost:6688/api/chats
Content-Type: application/json
Authorization: Bearer {{token1}}

{
"name": "project Y",
"members": [2, 3],
"public": false
}

### create direct chat without me
POST http://localhost:6688/api/chats
Content-Type: application/json
Authorization: Bearer {{token1}}

{
"members": [2, 3],
"public": false
}


### get chat list

Expand Down

0 comments on commit bb202b0

Please sign in to comment.