reworked api errors, now returning proper error codes

This commit is contained in:
2026-01-28 19:51:23 +01:00
parent 30dc7475c2
commit fbf7eea59a
12 changed files with 463 additions and 563 deletions
+62 -295
View File
@@ -7,7 +7,7 @@ use axum::{
use sqlx::{PgPool, Pool, Postgres};
use uuid::Uuid;
use crate::{auth::verify_jwt, db::room_id_from_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},
routes::users::UserProfile,
@@ -93,10 +93,10 @@ pub async fn is_member(user_id: i32, room_id: i32, db: &Pool<Postgres>) -> bool
async fn list_rooms(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
) -> Result<Json<Vec<Room>>, (StatusCode, String)> {
) -> Result<Json<Vec<Room>>, APIError> {
let claims = verify_jwt(headers)?;
if claims.sub != claims.sub {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
return Err(APIError::InvalidToken);
}
let user_id = user_id_from_uuid(&db, claims.sub).await?;
@@ -137,9 +137,16 @@ async fn create_room(
Extension(db): Extension<PgPool>,
headers: HeaderMap,
Json(payload): Json<NewRoomPayload>,
) -> Result<(StatusCode, Json<Room>), (StatusCode, String)> {
) -> 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();
@@ -154,8 +161,7 @@ async fn create_room(
// .bind(&payload.global)
.bind(false) // We do not allow global rooms
.execute(&db)
.await
.map_err(|_| (StatusCode::BAD_REQUEST, format!("Could not create room")))?;
.await?;
let room_id = room_id_from_uuid(&db, room_uuid).await?;
@@ -164,14 +170,9 @@ async fn create_room(
.bind(user_id)
.bind(room_id)
.execute(&db)
.await
.map_err(|_| (StatusCode::BAD_REQUEST, format!("Could not create room")))?;
.await?;
let owner_name = sqlx::query_scalar("SELECT username FROM user_ WHERE id = $1")
.bind(user_id)
.fetch_one(&db)
.await
.map_err(|_| (StatusCode::BAD_REQUEST, format!("Could not create room")))?;
let owner_name = username_from_id(&db, user_id).await?;
Ok((
StatusCode::CREATED,
@@ -190,7 +191,7 @@ async fn get_room(
Path(room_uuid): Path<Uuid>,
headers: HeaderMap,
Extension(db): Extension<PgPool>,
) -> Result<Json<Room>, (StatusCode, String)> {
) -> Result<Json<Room>, APIError> {
let claims = verify_jwt(headers)?;
let user_id = user_id_from_uuid(&db, claims.sub).await?;
@@ -232,16 +233,10 @@ async fn get_room(
.bind(user_id)
.fetch_one(&db)
.await
.map_err(|e| {
tracing::error!("Failed getting room: {e}");
(StatusCode::NOT_FOUND, "Room not found".to_string())
})?;
.map_err(|_| APIError::RoomNotFound)?;
if !row.is_member.unwrap_or(false) {
return Err((
StatusCode::FORBIDDEN,
"You are not a member of this room".to_string(),
));
return Err(APIError::NotAMember);
}
Ok(Json(Room {
@@ -257,7 +252,7 @@ async fn get_room(
async fn list_invites(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
) -> Result<Json<Vec<RoomInvite>>, (StatusCode, String)> {
) -> Result<Json<Vec<RoomInvite>>, APIError> {
let claims = verify_jwt(headers)?;
let user_id = user_id_from_uuid(&db, claims.sub).await?;
@@ -276,14 +271,7 @@ async fn list_invites(
)
.bind(user_id)
.fetch_all(&db)
.await
.map_err(|e| {
tracing::error!("{e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not list room invites".into(),
)
})?;
.await?;
Ok(Json(requests))
}
@@ -292,7 +280,7 @@ async fn send_invite(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
Json(payload): Json<SendRoomInvitePayload>,
) -> Result<(StatusCode, Json<RoomInvite>), (StatusCode, String)> {
) -> Result<(StatusCode, Json<RoomInvite>), APIError> {
let claims = verify_jwt(headers)?;
let sender_id = user_id_from_uuid(&db, claims.sub).await?;
@@ -300,10 +288,7 @@ async fn send_invite(
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(),
));
return Err(APIError::InviteSelf);
}
let is_already_member = sqlx::query_scalar::<_, bool>(
@@ -318,14 +303,10 @@ async fn send_invite(
.bind(receiver_id)
.bind(room_id)
.fetch_one(&db)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()))?;
.await?;
if is_already_member {
return Err((
StatusCode::CONFLICT,
"This user is already a member of this room".into(),
));
return Err(APIError::AlreadyMember);
}
sqlx::query("INSERT INTO room_invite_ (sender, receiver, room) VALUES ($1, $2, $3)")
@@ -334,12 +315,7 @@ async fn send_invite(
.bind(room_id)
.execute(&db)
.await
.map_err(|_| {
(
StatusCode::CONFLICT,
"You have already invited this user".into(),
)
})?;
.map_err(|_| APIError::AlreadyInvited)?;
let room_name = room_name_from_uuid(&db, payload.room_uuid).await?;
@@ -358,16 +334,13 @@ async fn accept_request(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
Json(payload): Json<AcceptRoomInvitePayload>,
) -> Result<(StatusCode, Json<Room>), (StatusCode, String)> {
) -> 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
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
let mut tx = db.begin().await?;
let rows = sqlx::query(
r#"
@@ -378,12 +351,11 @@ async fn accept_request(
.bind(sender_id)
.bind(receiver_id)
.execute(&mut *tx)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?
.await?
.rows_affected();
if rows == 0 {
return Err((StatusCode::NOT_FOUND, "No such invite".into()));
return Err(APIError::InviteNotFound);
}
let room_id = room_id_from_uuid(&db, payload.room_uuid).await?;
@@ -393,12 +365,7 @@ async fn accept_request(
.bind(room_id)
.execute(&mut *tx)
.await
.map_err(|_| {
(
StatusCode::CONFLICT,
"Error creating room membership".into(),
)
})?;
.map_err(|_| APIError::AlreadyMember)?;
let room: Room = sqlx::query_as(
r#"
@@ -418,19 +385,9 @@ async fn accept_request(
.bind(sender_id)
.fetch_one(&db)
.await
.map_err(|_| {
(
StatusCode::NOT_FOUND,
"Room not found or wrong owner".into(),
)
})?;
.map_err(|_| APIError::RoomNotFound)?;
tx.commit().await.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not accept room invite".into(),
)
})?;
tx.commit().await?;
Ok((
StatusCode::CREATED,
@@ -449,7 +406,7 @@ async fn decline_request(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
Json(payload): Json<AcceptRoomInvitePayload>,
) -> Result<StatusCode, (StatusCode, String)> {
) -> Result<StatusCode, APIError> {
let claims = verify_jwt(headers)?;
let receiver_id = user_id_from_uuid(&db, claims.sub).await?;
@@ -464,17 +421,11 @@ async fn decline_request(
.bind(sender_id)
.bind(receiver_id)
.execute(&db)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not decline the room invite".into(),
)
})?
.await?
.rows_affected();
if rows == 0 {
return Err((StatusCode::NOT_FOUND, "No such invite".into()));
return Err(APIError::InviteNotFound);
}
Ok(StatusCode::CREATED)
@@ -484,110 +435,25 @@ async fn leave_room(
headers: HeaderMap,
Path(room_uuid): Path<Uuid>,
Extension(db): Extension<PgPool>,
) -> Result<StatusCode, (StatusCode, String)> {
) -> 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((
StatusCode::FORBIDDEN,
"You are not a member of this room.".into(),
));
return Err(APIError::NotAMember);
}
let mut tx = db
.begin()
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
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
.map_err(|e| {
tracing::error!("Failed to get room owner: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to get room owner".into(),
)
})?;
.await?;
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(),
// )
// })?;
// }
return Err(APIError::RoomOwnerCannotLeave);
}
sqlx::query(
@@ -599,15 +465,9 @@ async fn leave_room(
.bind(user_id)
.bind(room_id)
.execute(&mut *tx)
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
.await?;
tx.commit().await.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not accept room invite".into(),
)
})?;
tx.commit().await?;
Ok(StatusCode::OK)
}
@@ -616,58 +476,35 @@ async fn transfer_ownership(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
Json(payload): Json<TransferOwnershipPayload>,
) -> Result<StatusCode, (StatusCode, String)> {
) -> 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((
StatusCode::FORBIDDEN,
"You are not a member of this room.".into(),
));
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
.map_err(|e| {
tracing::error!("Failed to get owner for room {room_id}: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to get room owner".into(),
)
})?;
.await?;
if owner != user_id {
return Err((
StatusCode::FORBIDDEN,
"You are not a member of this room.".into(),
));
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
.map_err(|e| {
tracing::error!("Failed to check user existence: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to check user existence".into(),
)
})?;
.await?;
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(),
));
return Err(APIError::UserNotFound);
}
let new_owner_id = user_id_from_uuid(&db, payload.new_owner_uuid).await?;
@@ -676,14 +513,7 @@ async fn transfer_ownership(
.bind(new_owner_id)
.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(),
)
})?;
.await?;
Ok(StatusCode::OK)
}
@@ -692,36 +522,23 @@ async fn list_members(
headers: HeaderMap,
Path(room_uuid): Path<Uuid>,
Extension(db): Extension<PgPool>,
) -> Result<(StatusCode, Json<Vec<UserProfile>>), (StatusCode, String)> {
) -> 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((
StatusCode::FORBIDDEN,
"You are not a member of this room.".into(),
));
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
.map_err(|e| {
tracing::error!("Failed to get global boolean {room_id}: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to fetch room".into(),
)
})?;
.await?;
if is_global {
return Err((
StatusCode::FORBIDDEN,
"Cannot get member list for global rooms".into(),
));
return Err(APIError::GlobalRoomMemberError);
}
let members = sqlx::query_as::<_, UserProfile>(
@@ -735,14 +552,7 @@ async fn list_members(
)
.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(),
)
})?;
.await?;
Ok((StatusCode::OK, Json(members)))
}
@@ -751,86 +561,43 @@ async fn delete_room(
headers: HeaderMap,
Path(room_uuid): Path<Uuid>,
Extension(db): Extension<PgPool>,
) -> Result<StatusCode, (StatusCode, String)> {
) -> 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((
StatusCode::FORBIDDEN,
"You are not a member of this room.".into(),
));
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
.map_err(|e| {
tracing::error!("Failed to get owner for room {room_id}: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to get room owner".into(),
)
})?;
.await?;
if owner != user_id {
return Err((
StatusCode::FORBIDDEN,
"You are not a member of this room.".into(),
));
return Err(APIError::NotAMember);
}
let mut tx = db
.begin()
.await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
let mut tx = db.begin().await?;
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(),
)
})?;
.await?;
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(),
)
})?;
.await?;
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(),
)
})?;
.await?;
tx.commit().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?;
Ok(StatusCode::OK)
}