added room actions: leave, delete and transfer ownership
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -619,7 +619,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "frangipane"
|
name = "frangipane"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "frangipane"
|
name = "frangipane"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ use axum::{
|
|||||||
Extension, Json, Router,
|
Extension, Json, Router,
|
||||||
extract::Path,
|
extract::Path,
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
routing::{get, post},
|
routing::{delete, get, post},
|
||||||
};
|
};
|
||||||
use sqlx::{PgPool, Pool, Postgres};
|
use sqlx::{PgPool, Pool, Postgres};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::db::{id_from_username, room_name_from_uuid, 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};
|
||||||
|
use crate::{
|
||||||
|
db::{id_from_username, room_name_from_uuid, user_id_from_uuid, username_from_id},
|
||||||
|
routes::users::UserProfile,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, serde::Serialize)]
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
@@ -46,15 +49,25 @@ pub struct AcceptRoomInvitePayload {
|
|||||||
pub sender_uuid: Uuid,
|
pub sender_uuid: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct TransferOwnershipPayload {
|
||||||
|
pub room_uuid: Uuid,
|
||||||
|
pub new_owner_uuid: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/rooms", get(list_rooms))
|
.route("/rooms", get(list_rooms))
|
||||||
.route("/rooms", post(create_room))
|
.route("/rooms", post(create_room))
|
||||||
.route("/rooms/{room_id}", get(get_room))
|
.route("/rooms/{room_uuid}", get(get_room))
|
||||||
|
.route("/rooms/{room-uuid}/members", get(list_members))
|
||||||
.route("/rooms/invites", get(list_invites))
|
.route("/rooms/invites", get(list_invites))
|
||||||
.route("/rooms/invite", post(send_invite))
|
.route("/rooms/invite", post(send_invite))
|
||||||
.route("/rooms/join", post(accept_request))
|
.route("/rooms/join", post(accept_request))
|
||||||
.route("/rooms/decline", post(decline_request))
|
.route("/rooms/decline", post(decline_request))
|
||||||
|
.route("/rooms/{room_uuid}/leave", post(leave_room))
|
||||||
|
.route("/rooms/{room_uuid}/delete", delete(delete_room))
|
||||||
|
.route("/rooms/transfer-ownership", post(transfer_ownership))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -138,7 +151,8 @@ async fn create_room(
|
|||||||
.bind(room_uuid)
|
.bind(room_uuid)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(&payload.name)
|
.bind(&payload.name)
|
||||||
.bind(&payload.global)
|
// .bind(&payload.global)
|
||||||
|
.bind(false) // We do not allow global rooms
|
||||||
.execute(&db)
|
.execute(&db)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, format!("Could not create room")))?;
|
.map_err(|_| (StatusCode::BAD_REQUEST, format!("Could not create room")))?;
|
||||||
@@ -465,3 +479,356 @@ async fn decline_request(
|
|||||||
|
|
||||||
Ok(StatusCode::CREATED)
|
Ok(StatusCode::CREATED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn leave_room(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(room_uuid): Path<Uuid>,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
) -> Result<StatusCode, (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
|
||||||
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
let room_id = room_id_from_uuid(&db, room_uuid).await?;
|
||||||
|
|
||||||
|
if !is_member(user_id, room_id, &db).await {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You are not a member of this room.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = db
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||||
|
|
||||||
|
let owner: i32 = sqlx::query_scalar(r#"SELECT owner FROM room_ WHERE id = $1"#)
|
||||||
|
.bind(room_id)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to get room owner: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get room owner".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if owner == user_id {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You cannot leave a room that you own".into(),
|
||||||
|
));
|
||||||
|
// let member_count: i64 =
|
||||||
|
// sqlx::query_scalar(r#"SELECT count(*) FROM membership_ WHERE room = $1"#)
|
||||||
|
// .bind(room_id)
|
||||||
|
// .fetch_one(&mut *tx)
|
||||||
|
// .await
|
||||||
|
// .map_err(|e| {
|
||||||
|
// tracing::error!("Failed to get member count: {e}");
|
||||||
|
// (
|
||||||
|
// StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
// "Failed to get member count".into(),
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
//
|
||||||
|
// if member_count > 0 {
|
||||||
|
// if let Some(new_owner) = payload.new_owner_uuid {
|
||||||
|
// let exists: bool =
|
||||||
|
// sqlx::query_scalar(r#"SELECT EXISTS (SELECT 1 FROM user_ WHERE uuid = $1)"#)
|
||||||
|
// .bind(new_owner)
|
||||||
|
// .fetch_one(&mut *tx)
|
||||||
|
// .await
|
||||||
|
// .map_err(|e| {
|
||||||
|
// tracing::error!("Failed to check user existence: {e}");
|
||||||
|
// (
|
||||||
|
// StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
// "Failed to check user existence".into(),
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
//
|
||||||
|
// if !exists {
|
||||||
|
// tracing::debug!(
|
||||||
|
// "User {user_id} tried to leave a room without transfering ownership"
|
||||||
|
// );
|
||||||
|
// return Err((
|
||||||
|
// StatusCode::FORBIDDEN,
|
||||||
|
// "Tried to transfer ownership to nonexistant user".into(),
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// sqlx::query("UPDATE room_ SET owner = $1 WHERE id = $2")
|
||||||
|
// .bind(new_owner)
|
||||||
|
// .bind(room_id)
|
||||||
|
// .execute(&mut *tx)
|
||||||
|
// .await
|
||||||
|
// .map_err(|e| {
|
||||||
|
// tracing::error!("Failed to set new owner: {e}");
|
||||||
|
// (
|
||||||
|
// StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
// "Failed to set new owner".into(),
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
// } else {
|
||||||
|
// return Err((
|
||||||
|
// StatusCode::BAD_REQUEST,
|
||||||
|
// "Please provide a new owner for a non-empty room".into(),
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// sqlx::query("DELETE FROM room_ WHERE id = $1")
|
||||||
|
// .bind(room_id)
|
||||||
|
// .execute(&mut *tx)
|
||||||
|
// .await
|
||||||
|
// .map_err(|e| {
|
||||||
|
// tracing::error!("Failed to delete room: {e}");
|
||||||
|
// (
|
||||||
|
// StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
// "Failed to delete room".into(),
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM membership_
|
||||||
|
WHERE user_id = $1 AND room = $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||||
|
|
||||||
|
tx.commit().await.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Could not accept room invite".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn transfer_ownership(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
Json(payload): Json<TransferOwnershipPayload>,
|
||||||
|
) -> Result<StatusCode, (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
|
||||||
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
let room_id = room_id_from_uuid(&db, payload.room_uuid).await?;
|
||||||
|
|
||||||
|
if !is_member(user_id, room_id, &db).await {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You are not a member of this room.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let owner: i32 = sqlx::query_scalar(r#"SELECT owner FROM room_ WHERE id = $1"#)
|
||||||
|
.bind(room_id)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to get owner for room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get room owner".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if owner != user_id {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You are not a member of this room.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let exists: bool = sqlx::query_scalar(r#"SELECT EXISTS (SELECT 1 FROM user_ WHERE uuid = $1)"#)
|
||||||
|
.bind(payload.new_owner_uuid)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to check user existence: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to check user existence".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
tracing::debug!(
|
||||||
|
"User {user_id} tried to leave room {room_id} without transfering ownership"
|
||||||
|
);
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"Tried to transfer ownership to nonexistant user".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query("UPDATE room_ SET owner = $1 WHERE id = $2")
|
||||||
|
.bind(payload.new_owner_uuid)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to set new owner for room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to set new owner".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_members(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(room_uuid): Path<Uuid>,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
) -> Result<(StatusCode, Json<Vec<UserProfile>>), (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
|
||||||
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
let room_id = room_id_from_uuid(&db, room_uuid).await?;
|
||||||
|
|
||||||
|
if !is_member(user_id, room_id, &db).await {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You are not a member of this room.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_global: bool = sqlx::query_scalar("SELECT global FROM room_ WHERE id = $1")
|
||||||
|
.bind(room_id)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to get global boolean {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to fetch room".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if is_global {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"Cannot get member list for global rooms".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let members = sqlx::query_as::<_, UserProfile>(
|
||||||
|
r#"
|
||||||
|
SELECT u.uuid, u.username
|
||||||
|
FROM user_ u
|
||||||
|
JOIN membership_ m
|
||||||
|
ON m.room = $1
|
||||||
|
AND m.user_id = u.id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(room_id)
|
||||||
|
.fetch_all(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to get member list for room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get member list".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((StatusCode::OK, Json(members)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_room(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(room_uuid): Path<Uuid>,
|
||||||
|
Extension(db): Extension<PgPool>,
|
||||||
|
) -> Result<StatusCode, (StatusCode, String)> {
|
||||||
|
let claims = verify_jwt(headers)?;
|
||||||
|
|
||||||
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||||
|
let room_id = room_id_from_uuid(&db, room_uuid).await?;
|
||||||
|
|
||||||
|
if !is_member(user_id, room_id, &db).await {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You are not a member of this room.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let owner: i32 = sqlx::query_scalar(r#"SELECT owner FROM room_ WHERE id = $1"#)
|
||||||
|
.bind(room_id)
|
||||||
|
.fetch_one(&db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to get owner for room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get room owner".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if owner != user_id {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"You are not a member of this room.".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = db
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||||
|
|
||||||
|
sqlx::query(r#"DELETE FROM message_ WHERE room = $1"#)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to delete messages on room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to delete messages".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
sqlx::query(r#"DELETE FROM membership_ WHERE room = $1"#)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to delete room memberships {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to delete room memberships".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
sqlx::query(r#"DELETE FROM room_ WHERE id = $1"#)
|
||||||
|
.bind(room_id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Failed to delete room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to delete room".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tx.commit().await.map_err(|e| {
|
||||||
|
tracing::error!("Failed to delete room {room_id}: {e}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to delete room".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ pub struct User {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||||
|
pub struct UserProfile {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct LoginPayload {
|
pub struct LoginPayload {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user