added room invites in backend

This commit is contained in:
2025-12-31 10:21:17 +01:00
parent 06b3a6f405
commit bd99c82d2d
4 changed files with 232 additions and 42 deletions

View File

@@ -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?;

View File

@@ -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<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(
Path(user_uuid): Path<Uuid>,
headers: HeaderMap,
Extension(db): Extension<PgPool>,
) -> Result<Json<Vec<Room>>, (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<PgPool>,
// ) -> Result<Json<Room>, (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<Uuid>,
headers: HeaderMap,
Extension(db): Extension<PgPool>,
) -> Result<Json<Room>, (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<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,
}),
))
}

View File

@@ -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();