771 lines
22 KiB
Rust
771 lines
22 KiB
Rust
use axum::{
|
|
Extension, Json, Router,
|
|
extract::Path,
|
|
http::{HeaderMap, StatusCode},
|
|
routing::{delete, get, post},
|
|
};
|
|
use sqlx::{PgPool, Pool, Postgres};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{MAX_ROOM_NAME_LENGTH, auth::verify_jwt, db::room_id_from_uuid, errors::APIError};
|
|
use crate::{
|
|
db::{id_from_username, room_name_from_uuid, user_id_from_uuid, username_from_id},
|
|
errors::ErrorResponse,
|
|
routes::users::UserProfile,
|
|
};
|
|
|
|
#[derive(sqlx::FromRow, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct Room {
|
|
pub uuid: Uuid,
|
|
pub name: String,
|
|
pub global: bool,
|
|
pub owner_name: String,
|
|
pub owner_uuid: Uuid,
|
|
pub unread_count: i64,
|
|
}
|
|
|
|
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct NewRoomPayload {
|
|
pub name: String,
|
|
pub global: bool,
|
|
}
|
|
|
|
#[derive(sqlx::FromRow, serde::Serialize, utoipa::ToSchema)]
|
|
pub struct RoomInvite {
|
|
pub room_uuid: Uuid,
|
|
pub room_name: String,
|
|
pub sender_uuid: Uuid,
|
|
pub sender_username: String,
|
|
}
|
|
|
|
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct SendRoomInvitePayload {
|
|
pub room_uuid: Uuid,
|
|
pub receiver_username: String,
|
|
}
|
|
|
|
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct AcceptRoomInvitePayload {
|
|
pub room_uuid: Uuid,
|
|
pub sender_uuid: Uuid,
|
|
}
|
|
|
|
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct TransferOwnershipPayload {
|
|
pub room_uuid: Uuid,
|
|
pub new_owner_uuid: Uuid,
|
|
}
|
|
|
|
pub fn routes() -> Router {
|
|
Router::new()
|
|
.route("/rooms", get(list_rooms))
|
|
.route("/rooms", post(create_room))
|
|
.route("/rooms/{room_uuid}", get(get_room))
|
|
.route("/rooms/{room_uuid}/members", get(list_members))
|
|
.route("/rooms/invites", get(list_invites))
|
|
.route("/rooms/invite", post(send_invite))
|
|
.route("/rooms/join", post(accept_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 {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT r.global OR EXISTS (
|
|
SELECT 1
|
|
FROM membership_ m
|
|
WHERE m.user_id = $1
|
|
AND m.room = r.id
|
|
)
|
|
FROM room_ r
|
|
WHERE r.id = $2
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(room_id)
|
|
.fetch_one(db)
|
|
.await
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/rooms",
|
|
responses(
|
|
(status = 200, description = "Successfully listed rooms", body = [Room]),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 404, description = "User not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn list_rooms(
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
) -> Result<Json<Vec<Room>>, APIError> {
|
|
let claims = verify_jwt(headers)?;
|
|
|
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
|
|
|
let rooms = sqlx::query_as::<_, Room>(
|
|
r#"
|
|
SELECT
|
|
r.uuid,
|
|
u.username AS owner_name,
|
|
u.uuid AS owner_uuid,
|
|
r.name,
|
|
r.global,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM message_ m
|
|
WHERE m.room = r.id
|
|
AND m.sent_at > mem.last_read_at
|
|
AND m.sender != $1
|
|
) AS unread_count
|
|
FROM room_ r
|
|
JOIN user_ u ON u.id = r.owner
|
|
LEFT JOIN membership_ mem ON mem.room = r.id AND mem.user_id = $1
|
|
WHERE r.global OR mem.user_id IS NOT NULL
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.fetch_all(&db)
|
|
.await
|
|
.unwrap_or_else(|e| {
|
|
tracing::error!("Failed listing rooms: {e}");
|
|
Vec::new()
|
|
});
|
|
|
|
Ok(Json(rooms))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/rooms",
|
|
request_body = NewRoomPayload,
|
|
responses(
|
|
(status = 201, description = "Room successfully created", body = Room),
|
|
(status = 400, description = "Room name must be 0-35 characters long", body = ErrorResponse),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn create_room(
|
|
Extension(db): Extension<PgPool>,
|
|
headers: HeaderMap,
|
|
Json(payload): Json<NewRoomPayload>,
|
|
) -> Result<(StatusCode, Json<Room>), APIError> {
|
|
let claims = verify_jwt(headers)?;
|
|
|
|
{
|
|
let room_name_length = payload.name.len();
|
|
if room_name_length > MAX_ROOM_NAME_LENGTH || room_name_length < 1 {
|
|
return Err(APIError::RoomNameLength);
|
|
}
|
|
}
|
|
|
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
|
|
|
let room_uuid = uuid::Uuid::now_v7();
|
|
|
|
sqlx::query(
|
|
"INSERT INTO room_ (uuid, owner, name, global)
|
|
VALUES ($1, $2, $3, $4)",
|
|
)
|
|
.bind(room_uuid)
|
|
.bind(user_id)
|
|
.bind(&payload.name)
|
|
// .bind(&payload.global)
|
|
.bind(false) // We do not allow global rooms
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
let room_id = room_id_from_uuid(&db, room_uuid).await?;
|
|
|
|
// We do this even for the owner
|
|
sqlx::query("INSERT INTO membership_ (user_id, room) VALUES ($1, $2)")
|
|
.bind(user_id)
|
|
.bind(room_id)
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
let owner_name = username_from_id(&db, user_id).await?;
|
|
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(Room {
|
|
uuid: room_uuid,
|
|
owner_name,
|
|
owner_uuid: claims.sub,
|
|
name: payload.name,
|
|
global: payload.global,
|
|
unread_count: 0,
|
|
}),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/rooms/{room_uuid}",
|
|
params(
|
|
("room_uuid" = Uuid, Path, description = "UUID of the room to retrieve")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Room successfully retrieved", body = Room),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 403, description = "You are not a member of this room", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn get_room(
|
|
Path(room_uuid): Path<Uuid>,
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
) -> Result<Json<Room>, APIError> {
|
|
let claims = verify_jwt(headers)?;
|
|
|
|
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
|
|
|
#[derive(sqlx::FromRow)]
|
|
struct RoomRow {
|
|
uuid: Uuid,
|
|
owner_name: String,
|
|
owner_uuid: Uuid,
|
|
name: String,
|
|
global: bool,
|
|
unread_count: Option<i64>,
|
|
is_member: Option<bool>,
|
|
}
|
|
|
|
let row: RoomRow = sqlx::query_as(
|
|
r#"
|
|
SELECT
|
|
r.uuid,
|
|
u.username AS owner_name,
|
|
u.uuid AS owner_uuid,
|
|
r.name,
|
|
r.global,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM message_ m
|
|
WHERE m.room = r.id
|
|
AND m.sent_at > mem.last_read_at
|
|
AND m.sender != $2
|
|
) AS unread_count,
|
|
(r.global OR mem.user_id IS NOT NULL) AS is_member
|
|
FROM room_ r
|
|
JOIN user_ u ON u.id = r.owner
|
|
LEFT JOIN membership_ mem ON mem.room = r.id AND mem.user_id = $2
|
|
WHERE r.uuid = $1
|
|
"#,
|
|
)
|
|
.bind(room_uuid)
|
|
.bind(user_id)
|
|
.fetch_one(&db)
|
|
.await
|
|
.map_err(|_| APIError::RoomNotFound)?;
|
|
|
|
if !row.is_member.unwrap_or(false) {
|
|
return Err(APIError::NotAMember);
|
|
}
|
|
|
|
Ok(Json(Room {
|
|
uuid: row.uuid,
|
|
owner_name: row.owner_name,
|
|
owner_uuid: row.owner_uuid,
|
|
name: row.name,
|
|
global: row.global,
|
|
unread_count: row.unread_count.unwrap_or(0),
|
|
}))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/rooms/invites",
|
|
responses(
|
|
(status = 200, description = "List of room invites successfully retrieved", body = [RoomInvite]),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 404, description = "User not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn list_invites(
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
) -> Result<Json<Vec<RoomInvite>>, APIError> {
|
|
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,
|
|
r.name AS room_name,
|
|
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?;
|
|
|
|
Ok(Json(requests))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/rooms/invite",
|
|
request_body = SendRoomInvitePayload,
|
|
responses(
|
|
(status = 201, description = "Room invite successfully sent", body = RoomInvite),
|
|
(status = 400, description = "Cannot invite yourself", body = ErrorResponse),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 409, description = "User is already a member or Invite already sent", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn send_invite(
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
Json(payload): Json<SendRoomInvitePayload>,
|
|
) -> Result<(StatusCode, Json<RoomInvite>), APIError> {
|
|
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(APIError::InviteSelf);
|
|
}
|
|
|
|
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?;
|
|
|
|
if is_already_member {
|
|
return Err(APIError::AlreadyMember);
|
|
}
|
|
|
|
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(|_| APIError::AlreadyInvited)?;
|
|
|
|
let room_name = room_name_from_uuid(&db, payload.room_uuid).await?;
|
|
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(RoomInvite {
|
|
room_uuid: payload.room_uuid,
|
|
room_name,
|
|
sender_uuid: claims.sub,
|
|
sender_username: username_from_id(&db, receiver_id).await?,
|
|
}),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/rooms/join",
|
|
request_body = AcceptRoomInvitePayload,
|
|
responses(
|
|
(status = 201, description = "Room invite successfully accepted", body = Room),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 404, description = "User not found, Room not found, or Invite not found", body = ErrorResponse),
|
|
(status = 409, description = "User is already a member", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn accept_request(
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
Json(payload): Json<AcceptRoomInvitePayload>,
|
|
) -> Result<(StatusCode, Json<Room>), APIError> {
|
|
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?;
|
|
|
|
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?
|
|
.rows_affected();
|
|
|
|
if rows == 0 {
|
|
return Err(APIError::InviteNotFound);
|
|
}
|
|
|
|
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(|_| APIError::AlreadyMember)?;
|
|
|
|
let room: Room = sqlx::query_as(
|
|
r#"
|
|
SELECT
|
|
r.uuid,
|
|
u.username AS owner_name,
|
|
u.uuid AS owner_uuid,
|
|
r.name,
|
|
r.global,
|
|
0::bigint AS unread_count
|
|
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(|_| APIError::RoomNotFound)?;
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(Room {
|
|
uuid: payload.room_uuid,
|
|
owner_name: room.owner_name,
|
|
owner_uuid: room.owner_uuid,
|
|
name: room.name,
|
|
global: room.global,
|
|
unread_count: room.unread_count,
|
|
}),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/rooms/decline",
|
|
request_body = AcceptRoomInvitePayload,
|
|
responses(
|
|
(status = 201, description = "Room invite successfully declined"),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Invite not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn decline_request(
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
Json(payload): Json<AcceptRoomInvitePayload>,
|
|
) -> Result<StatusCode, APIError> {
|
|
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 rows = sqlx::query(
|
|
r#"
|
|
DELETE FROM room_invite_
|
|
WHERE sender = $1 AND receiver = $2
|
|
"#,
|
|
)
|
|
.bind(sender_id)
|
|
.bind(receiver_id)
|
|
.execute(&db)
|
|
.await?
|
|
.rows_affected();
|
|
|
|
if rows == 0 {
|
|
return Err(APIError::InviteNotFound);
|
|
}
|
|
|
|
Ok(StatusCode::CREATED)
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/rooms/{room_uuid}/leave",
|
|
params(
|
|
("room_uuid" = Uuid, Path, description = "UUID of the room to leave")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Successfully left the room"),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 403, description = "You are not a member of this room or Owner cannot leave the room without transferring ownership", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn leave_room(
|
|
headers: HeaderMap,
|
|
Path(room_uuid): Path<Uuid>,
|
|
Extension(db): Extension<PgPool>,
|
|
) -> Result<StatusCode, APIError> {
|
|
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(APIError::NotAMember);
|
|
}
|
|
|
|
let mut tx = db.begin().await?;
|
|
|
|
let owner: i32 = sqlx::query_scalar(r#"SELECT owner FROM room_ WHERE id = $1"#)
|
|
.bind(room_id)
|
|
.fetch_one(&mut *tx)
|
|
.await?;
|
|
|
|
if owner == user_id {
|
|
return Err(APIError::RoomOwnerCannotLeave);
|
|
}
|
|
|
|
sqlx::query(
|
|
r#"
|
|
DELETE FROM membership_
|
|
WHERE user_id = $1 AND room = $2
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.bind(room_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(StatusCode::OK)
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/rooms/transfer-ownership",
|
|
request_body = TransferOwnershipPayload,
|
|
responses(
|
|
(status = 200, description = "Room ownership successfully transferred"),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 403, description = "You are not a member of this room", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn transfer_ownership(
|
|
headers: HeaderMap,
|
|
Extension(db): Extension<PgPool>,
|
|
Json(payload): Json<TransferOwnershipPayload>,
|
|
) -> Result<StatusCode, APIError> {
|
|
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(APIError::NotAMember);
|
|
}
|
|
|
|
let owner: i32 = sqlx::query_scalar(r#"SELECT owner FROM room_ WHERE id = $1"#)
|
|
.bind(room_id)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
|
|
if owner != user_id {
|
|
return Err(APIError::NotAMember);
|
|
}
|
|
|
|
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?;
|
|
|
|
if !exists {
|
|
tracing::debug!(
|
|
"User {user_id} tried to leave room {room_id} without transfering ownership"
|
|
);
|
|
return Err(APIError::UserNotFound);
|
|
}
|
|
|
|
let new_owner_id = user_id_from_uuid(&db, payload.new_owner_uuid).await?;
|
|
|
|
sqlx::query("UPDATE room_ SET owner = $1 WHERE id = $2")
|
|
.bind(new_owner_id)
|
|
.bind(room_id)
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
Ok(StatusCode::OK)
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/rooms/{room_uuid}/members",
|
|
params(
|
|
("room_uuid" = Uuid, Path, description = "UUID of the room to list members of")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Room members successfully retrieved", body = [UserProfile]),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 403, description = "You are not a member of this room or Cannot list members for global rooms", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn list_members(
|
|
headers: HeaderMap,
|
|
Path(room_uuid): Path<Uuid>,
|
|
Extension(db): Extension<PgPool>,
|
|
) -> Result<(StatusCode, Json<Vec<UserProfile>>), APIError> {
|
|
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(APIError::NotAMember);
|
|
}
|
|
|
|
let is_global: bool = sqlx::query_scalar("SELECT global FROM room_ WHERE id = $1")
|
|
.bind(room_id)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
|
|
if is_global {
|
|
return Err(APIError::GlobalRoomMemberError);
|
|
}
|
|
|
|
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?;
|
|
|
|
Ok((StatusCode::OK, Json(members)))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
delete,
|
|
path = "/rooms/{room_uuid}/delete",
|
|
params(
|
|
("room_uuid" = Uuid, Path, description = "UUID of the room to delete")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Room successfully deleted"),
|
|
(status = 401, description = "Missing authentication header or Invalid or expired token", body = ErrorResponse),
|
|
(status = 403, description = "You are not a member of this room", body = ErrorResponse),
|
|
(status = 404, description = "User not found or Room not found", body = ErrorResponse),
|
|
(status = 500, description = "Database error", body = ErrorResponse)
|
|
),
|
|
security(
|
|
("bearer_auth" = [])
|
|
)
|
|
)]
|
|
async fn delete_room(
|
|
headers: HeaderMap,
|
|
Path(room_uuid): Path<Uuid>,
|
|
Extension(db): Extension<PgPool>,
|
|
) -> Result<StatusCode, APIError> {
|
|
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(APIError::NotAMember);
|
|
}
|
|
|
|
let owner: i32 = sqlx::query_scalar(r#"SELECT owner FROM room_ WHERE id = $1"#)
|
|
.bind(room_id)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
|
|
if owner != user_id {
|
|
return Err(APIError::NotAMember);
|
|
}
|
|
|
|
let mut tx = db.begin().await?;
|
|
|
|
sqlx::query(r#"DELETE FROM message_ WHERE room = $1"#)
|
|
.bind(room_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query(r#"DELETE FROM membership_ WHERE room = $1"#)
|
|
.bind(room_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
sqlx::query(r#"DELETE FROM room_ WHERE id = $1"#)
|
|
.bind(room_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(StatusCode::OK)
|
|
}
|