added room invites in backend
This commit is contained in:
@@ -61,7 +61,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// )
|
// )
|
||||||
.layer(cors);
|
.layer(cors);
|
||||||
|
|
||||||
let port = var("CHATAPP_SERVER_PORT").unwrap_or_else(|_| "8081".to_string());
|
let port = var("CHATAPP_SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
|
||||||
let addr = format!("0.0.0.0:{port}");
|
let addr = format!("0.0.0.0:{port}");
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
|||||||
@@ -77,8 +77,7 @@ async fn list_messages(
|
|||||||
.bind(room_id)
|
.bind(room_id)
|
||||||
.fetch_all(&db)
|
.fetch_all(&db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|_| {
|
||||||
tracing::error!("failed to list messages: {e}");
|
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Failed to list messages".into(),
|
"Failed to list messages".into(),
|
||||||
@@ -128,12 +127,7 @@ async fn create_message(
|
|||||||
.bind(&payload.content)
|
.bind(&payload.content)
|
||||||
.fetch_one(&db)
|
.fetch_one(&db)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|_| (StatusCode::BAD_REQUEST, "Could not create message".into()))?;
|
||||||
(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
format!("Could not create message: {e}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let sender_name = username_from_uuid(&db, claims.sub).await?;
|
let sender_name = username_from_uuid(&db, claims.sub).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use axum::{
|
|||||||
use sqlx::{PgPool, Pool, Postgres};
|
use sqlx::{PgPool, Pool, Postgres};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::db::user_id_from_uuid;
|
use crate::db::{id_from_username, user_id_from_uuid, username_from_id};
|
||||||
use crate::{auth::verify_jwt, db::room_id_from_uuid};
|
use crate::{auth::verify_jwt, db::room_id_from_uuid};
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, serde::Serialize)]
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
@@ -24,11 +24,33 @@ pub struct NewRoomPayload {
|
|||||||
pub global: bool,
|
pub global: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
|
pub struct RoomInvite {
|
||||||
|
pub room_uuid: Uuid,
|
||||||
|
pub sender_uuid: Uuid,
|
||||||
|
pub sender_username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct SendRoomInvitePayload {
|
||||||
|
pub room_uuid: Uuid,
|
||||||
|
pub receiver_username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct AcceptRoomInvitePayload {
|
||||||
|
pub room_uuid: Uuid,
|
||||||
|
pub sender_uuid: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/rooms/{user_uuid}", get(list_rooms))
|
.route("/rooms", get(list_rooms))
|
||||||
.route("/rooms", post(create_room))
|
.route("/rooms", post(create_room))
|
||||||
// .route("/rooms/{user_uuid}/{room_id}", get(get_room))
|
.route("/rooms/{room_id}", get(get_room))
|
||||||
|
.route("/rooms/invites", get(list_invites))
|
||||||
|
.route("/rooms/invite", post(send_invite))
|
||||||
|
.route("/rooms/join", post(accept_request))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_member(user_id: i32, room_id: i32, db: &Pool<Postgres>) -> bool {
|
pub async fn is_member(user_id: i32, room_id: i32, db: &Pool<Postgres>) -> bool {
|
||||||
@@ -52,12 +74,11 @@ pub async fn is_member(user_id: i32, room_id: i32, db: &Pool<Postgres>) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn list_rooms(
|
async fn list_rooms(
|
||||||
Path(user_uuid): Path<Uuid>,
|
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(db): Extension<PgPool>,
|
Extension(db): Extension<PgPool>,
|
||||||
) -> Result<Json<Vec<Room>>, (StatusCode, String)> {
|
) -> Result<Json<Vec<Room>>, (StatusCode, String)> {
|
||||||
let claims = verify_jwt(headers)?;
|
let claims = verify_jwt(headers)?;
|
||||||
if claims.sub != user_uuid {
|
if claims.sub != claims.sub {
|
||||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,29 +157,200 @@ async fn create_room(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// async fn get_room(
|
async fn get_room(
|
||||||
// Path((user_uuid, room_uuid)): Path<(Uuid, Uuid)>,
|
Path(room_uuid): Path<Uuid>,
|
||||||
// headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
// Extension(db): Extension<PgPool>,
|
Extension(db): Extension<PgPool>,
|
||||||
// ) -> Result<Json<Room>, (StatusCode, String)> {
|
) -> Result<Json<Room>, (StatusCode, String)> {
|
||||||
// let claims = verify_jwt(headers)?;
|
let claims = verify_jwt(headers)?;
|
||||||
// if claims.sub != user_uuid {
|
|
||||||
// return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
// }
|
|
||||||
//
|
let room: Room = sqlx::query_as(
|
||||||
// let user_id = user_id_from_uuid(&db, user_uuid).await?;
|
r#"
|
||||||
//
|
SELECT uuid, u.name AS owner_name, r.name, r.global
|
||||||
// let room: Room =
|
FROM room_ r
|
||||||
// sqlx::query_as("SELECT uuid, owner, name FROM room_ WHERE uuid = $1 AND owner = $2")
|
JOIN user u ON u.id = r.owner
|
||||||
// .bind(room_uuid)
|
WHERE uuid = $1 AND owner = $2
|
||||||
// .bind(user_id)
|
"#,
|
||||||
// .fetch_one(&db)
|
)
|
||||||
// .await
|
.bind(room_uuid)
|
||||||
// .map_err(|_| (StatusCode::NOT_FOUND, "Room not found".to_string()))?;
|
.bind(user_id)
|
||||||
//
|
.fetch_one(&db)
|
||||||
// Ok(Json(Room {
|
.await
|
||||||
// uuid: room_uuid,
|
.map_err(|_| (StatusCode::NOT_FOUND, "Room not found".to_string()))?;
|
||||||
// owner: room.owner,
|
|
||||||
// name: room.name,
|
Ok(Json(Room {
|
||||||
// }))
|
uuid: room_uuid,
|
||||||
// }
|
owner_name: room.owner_name,
|
||||||
|
name: room.name,
|
||||||
|
global: room.global,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_invites(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
) -> Result<Json<Vec<RoomInvite>>, (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
|
||||||
|
let requests = sqlx::query_as::<_, RoomInvite>(
|
||||||
|
r#"
|
||||||
|
SELECT r.uuid AS room_uuid, u.uuid AS sender_uuid, u.username AS sender_username
|
||||||
|
FROM room_invite_ AS i
|
||||||
|
JOIN user_ u ON u.id = i.sender
|
||||||
|
JOIN room_ r ON r.id = i.room
|
||||||
|
WHERE i.receiver = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_all(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Could not list room invites".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(requests))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_invite(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
Json(payload): Json<SendRoomInvitePayload>,
|
||||||
|
) -> Result<(StatusCode, Json<RoomInvite>), (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
|
||||||
|
let sender_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
let receiver_id = id_from_username(&db, payload.receiver_username).await?;
|
||||||
|
let room_id = room_id_from_uuid(&db, payload.room_uuid).await?;
|
||||||
|
|
||||||
|
if sender_id == receiver_id {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Cannot send a room invite to yourself".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_already_member = sqlx::query_scalar::<_, bool>(
|
||||||
|
r#"
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM membership_
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND room = $2
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(receiver_id)
|
||||||
|
.bind(room_id)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()))?;
|
||||||
|
|
||||||
|
if is_already_member {
|
||||||
|
return Err((
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"This user is already a member of this room".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO room_invite_ (sender, receiver, room) VALUES ($1, $2, $3)")
|
||||||
|
.bind(sender_id)
|
||||||
|
.bind(receiver_id)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::CONFLICT, "Request already exists".into()))?;
|
||||||
|
|
||||||
|
tracing::info!("bro");
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(RoomInvite {
|
||||||
|
room_uuid: payload.room_uuid,
|
||||||
|
sender_uuid: claims.sub,
|
||||||
|
sender_username: username_from_id(&db, receiver_id).await?,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn accept_request(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
Json(payload): Json<AcceptRoomInvitePayload>,
|
||||||
|
) -> Result<(StatusCode, Json<Room>), (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
|
||||||
|
let receiver_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
let sender_id = user_id_from_uuid(&db, payload.sender_uuid).await?;
|
||||||
|
|
||||||
|
let mut tx = db
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||||
|
|
||||||
|
let rows = sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM room_invite_
|
||||||
|
WHERE sender = $1 AND receiver = $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(sender_id)
|
||||||
|
.bind(receiver_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
if rows == 0 {
|
||||||
|
return Err((StatusCode::NOT_FOUND, "No such invite".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_id = room_id_from_uuid(&db, payload.room_uuid).await?;
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO membership_ (user_id, room) VALUES ($1, $2)")
|
||||||
|
.bind(receiver_id)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"Error creating room membership".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let room: Room = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT r.uuid, u.username AS owner_name, r.name, r.global
|
||||||
|
FROM room_ r
|
||||||
|
JOIN user_ u ON u.id = r.owner
|
||||||
|
WHERE r.id = $1 AND r.owner = $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(room_id)
|
||||||
|
.bind(sender_id)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::NOT_FOUND, "Room not found".into()))?;
|
||||||
|
|
||||||
|
tx.commit().await.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Could not accept room invite".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(Room {
|
||||||
|
uuid: payload.room_uuid,
|
||||||
|
owner_name: room.owner_name,
|
||||||
|
name: room.name,
|
||||||
|
global: room.global,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,8 +112,12 @@ pub async fn register_user(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let password_hash =
|
let password_hash = hash_password(&payload.password).map_err(|_| {
|
||||||
hash_password(&payload.password).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to hash password".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let user_uuid = uuid::Uuid::now_v7();
|
let user_uuid = uuid::Uuid::now_v7();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user