From bd99c82d2de0a826c3579c03fecd0e2fc76374d8 Mon Sep 17 00:00:00 2001 From: eiiko6 Date: Wed, 31 Dec 2025 10:21:17 +0100 Subject: [PATCH] added room invites in backend --- src/main.rs | 2 +- src/routes/messages.rs | 10 +- src/routes/rooms.rs | 254 ++++++++++++++++++++++++++++++++++++----- src/routes/users.rs | 8 +- 4 files changed, 232 insertions(+), 42 deletions(-) diff --git a/src/main.rs b/src/main.rs index f184774..03d9f92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ async fn main() -> anyhow::Result<()> { // ) .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 listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); diff --git a/src/routes/messages.rs b/src/routes/messages.rs index 5620b1d..3eb563a 100644 --- a/src/routes/messages.rs +++ b/src/routes/messages.rs @@ -77,8 +77,7 @@ async fn list_messages( .bind(room_id) .fetch_all(&db) .await - .map_err(|e| { - tracing::error!("failed to list messages: {e}"); + .map_err(|_| { ( StatusCode::INTERNAL_SERVER_ERROR, "Failed to list messages".into(), @@ -128,12 +127,7 @@ async fn create_message( .bind(&payload.content) .fetch_one(&db) .await - .map_err(|e| { - ( - StatusCode::BAD_REQUEST, - format!("Could not create message: {e}"), - ) - })?; + .map_err(|_| (StatusCode::BAD_REQUEST, "Could not create message".into()))?; let sender_name = username_from_uuid(&db, claims.sub).await?; diff --git a/src/routes/rooms.rs b/src/routes/rooms.rs index 97491b3..d6a536c 100644 --- a/src/routes/rooms.rs +++ b/src/routes/rooms.rs @@ -7,7 +7,7 @@ use axum::{ use sqlx::{PgPool, Pool, Postgres}; 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}; #[derive(sqlx::FromRow, serde::Serialize)] @@ -24,11 +24,33 @@ pub struct NewRoomPayload { 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 { Router::new() - .route("/rooms/{user_uuid}", get(list_rooms)) + .route("/rooms", get(list_rooms)) .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) -> bool { @@ -52,12 +74,11 @@ pub async fn is_member(user_id: i32, room_id: i32, db: &Pool) -> bool } async fn list_rooms( - Path(user_uuid): Path, headers: HeaderMap, Extension(db): Extension, ) -> Result>, (StatusCode, String)> { let claims = verify_jwt(headers)?; - if claims.sub != user_uuid { + if claims.sub != claims.sub { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } @@ -136,29 +157,200 @@ async fn create_room( )) } -// async fn get_room( -// Path((user_uuid, room_uuid)): Path<(Uuid, Uuid)>, -// headers: HeaderMap, -// Extension(db): Extension, -// ) -> Result, (StatusCode, String)> { -// 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, user_uuid).await?; -// -// let room: Room = -// sqlx::query_as("SELECT uuid, owner, name FROM room_ WHERE uuid = $1 AND owner = $2") -// .bind(room_uuid) -// .bind(user_id) -// .fetch_one(&db) -// .await -// .map_err(|_| (StatusCode::NOT_FOUND, "Room not found".to_string()))?; -// -// Ok(Json(Room { -// uuid: room_uuid, -// owner: room.owner, -// name: room.name, -// })) -// } +async fn get_room( + Path(room_uuid): Path, + headers: HeaderMap, + Extension(db): Extension, +) -> Result, (StatusCode, String)> { + let claims = verify_jwt(headers)?; + + let user_id = user_id_from_uuid(&db, claims.sub).await?; + + let room: Room = sqlx::query_as( + r#" + SELECT uuid, u.name AS owner_name, r.name, r.global + FROM room_ r + JOIN user u ON u.id = r.owner + WHERE uuid = $1 AND owner = $2 + "#, + ) + .bind(room_uuid) + .bind(user_id) + .fetch_one(&db) + .await + .map_err(|_| (StatusCode::NOT_FOUND, "Room not found".to_string()))?; + + 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, +) -> Result>, (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, + Json(payload): Json, +) -> Result<(StatusCode, Json), (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, + Json(payload): Json, +) -> Result<(StatusCode, Json), (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, + }), + )) +} diff --git a/src/routes/users.rs b/src/routes/users.rs index ca87a80..eac0f25 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -112,8 +112,12 @@ pub async fn register_user( )); } - let password_hash = - hash_password(&payload.password).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; + let password_hash = hash_password(&payload.password).map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to hash password".into(), + ) + })?; let user_uuid = uuid::Uuid::now_v7();