reworked api errors, now returning proper error codes
This commit is contained in:
@@ -7,8 +7,11 @@ use axum::{
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{user_id_from_uuid, username_from_id, username_from_uuid};
|
||||
use crate::{auth::verify_jwt, db::id_from_username};
|
||||
use crate::{
|
||||
db::{user_id_from_uuid, username_from_id, username_from_uuid},
|
||||
errors::APIError,
|
||||
};
|
||||
|
||||
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||
pub struct Friend {
|
||||
@@ -56,7 +59,7 @@ pub fn routes() -> Router {
|
||||
async fn list_friends(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
) -> Result<Json<Vec<Friend>>, (StatusCode, String)> {
|
||||
) -> Result<Json<Vec<Friend>>, APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
|
||||
@@ -72,12 +75,7 @@ async fn list_friends(
|
||||
.bind(user_id)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not list friends".into(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|e| APIError::Internal(format!("Could not list friends: {e}")))?;
|
||||
|
||||
Ok(Json(friends))
|
||||
}
|
||||
@@ -85,7 +83,7 @@ async fn list_friends(
|
||||
async fn list_requests(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
) -> Result<Json<Vec<FriendRequest>>, (StatusCode, String)> {
|
||||
) -> Result<Json<Vec<FriendRequest>>, APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
|
||||
@@ -100,12 +98,7 @@ async fn list_requests(
|
||||
.bind(user_id)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not list friend requests".into(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|e| APIError::Internal(format!("Could not list friend requests: {e}")))?;
|
||||
|
||||
Ok(Json(requests))
|
||||
}
|
||||
@@ -114,17 +107,14 @@ async fn send_request(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<SendFriendRequestPayload>,
|
||||
) -> Result<(StatusCode, Json<FriendRequest>), (StatusCode, String)> {
|
||||
) -> Result<(StatusCode, Json<FriendRequest>), 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?;
|
||||
|
||||
if sender_id == receiver_id {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot send a friend request to yourself".into(),
|
||||
));
|
||||
return Err(APIError::FriendRequestSelf);
|
||||
}
|
||||
|
||||
let is_already_friend = sqlx::query_scalar::<_, bool>(
|
||||
@@ -139,14 +129,10 @@ async fn send_request(
|
||||
.bind(sender_id)
|
||||
.bind(receiver_id)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()))?;
|
||||
.await?;
|
||||
|
||||
if is_already_friend {
|
||||
return Err((
|
||||
StatusCode::CONFLICT,
|
||||
"You are already friends with this user".into(),
|
||||
));
|
||||
return Err(APIError::AlreadyFriends);
|
||||
}
|
||||
|
||||
sqlx::query("INSERT INTO friend_request_ (sender, receiver) VALUES ($1, $2)")
|
||||
@@ -154,12 +140,7 @@ async fn send_request(
|
||||
.bind(receiver_id)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::CONFLICT,
|
||||
"You have already send a friend request to this user".into(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|_| APIError::FriendRequestAlreadySent)?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
@@ -174,7 +155,7 @@ async fn accept_request(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<AcceptFriendRequestPayload>,
|
||||
) -> Result<(StatusCode, Json<Friend>), (StatusCode, String)> {
|
||||
) -> Result<(StatusCode, Json<Friend>), APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
let receiver_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
@@ -186,10 +167,7 @@ async fn accept_request(
|
||||
(receiver_id, sender_id)
|
||||
};
|
||||
|
||||
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#"
|
||||
@@ -201,12 +179,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 request".into()));
|
||||
return Err(APIError::FriendRequestNotFound);
|
||||
}
|
||||
|
||||
sqlx::query("INSERT INTO friendship_ (user_first, user_second) VALUES ($1, $2)")
|
||||
@@ -214,14 +191,9 @@ async fn accept_request(
|
||||
.bind(second)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::CONFLICT, "Already friends".into()))?;
|
||||
.map_err(|_| APIError::AlreadyFriends)?;
|
||||
|
||||
tx.commit().await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not accept friendship".into(),
|
||||
)
|
||||
})?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
@@ -236,7 +208,7 @@ async fn decline_request(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<DeclineFriendRequestPayload>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
) -> Result<StatusCode, APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
let receiver_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
@@ -252,17 +224,11 @@ async fn decline_request(
|
||||
.bind(sender_id)
|
||||
.bind(receiver_id)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not decline friend request".into(),
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
if rows == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "No such request".into()));
|
||||
return Err(APIError::FriendRequestNotFound);
|
||||
}
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
@@ -272,7 +238,7 @@ async fn is_friend(
|
||||
headers: HeaderMap,
|
||||
Path(target_uuid): Path<Uuid>,
|
||||
Extension(db): Extension<PgPool>,
|
||||
) -> Result<Json<bool>, (StatusCode, String)> {
|
||||
) -> Result<Json<bool>, APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
@@ -290,8 +256,7 @@ async fn is_friend(
|
||||
.bind(user_id)
|
||||
.bind(target_id)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()))?;
|
||||
.await?;
|
||||
|
||||
Ok(Json(is_friend))
|
||||
}
|
||||
@@ -300,7 +265,7 @@ async fn remove_friend(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<RemoveFriendPayload>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
) -> Result<StatusCode, APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
@@ -316,20 +281,11 @@ async fn remove_friend(
|
||||
.bind(user_id)
|
||||
.bind(friend_id)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not remove friend".into(),
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
if rows == 0 {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
"User is not in your friends list".into(),
|
||||
));
|
||||
return Err(APIError::NotFriends);
|
||||
}
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
|
||||
@@ -18,6 +18,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
auth::{verify_jwt, verify_jwt_string},
|
||||
db::room_id_from_uuid,
|
||||
errors::APIError,
|
||||
routes::{rooms::is_member, ws::WsAuthQuery},
|
||||
};
|
||||
use crate::{
|
||||
@@ -72,25 +73,19 @@ async fn list_messages(
|
||||
Query(query): Query<MessageFetchQuery>,
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
) -> Result<Json<Vec<Message>>, (StatusCode, String)> {
|
||||
) -> Result<Json<Vec<Message>>, 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,
|
||||
String::from("You are not a member of this room"),
|
||||
));
|
||||
return Err(APIError::NotAMember);
|
||||
}
|
||||
|
||||
let limit: i32 = query.limit.unwrap_or(30).abs().min(80);
|
||||
|
||||
let mut tx = db
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let messages = sqlx::query_as::<_, MessageRow>(
|
||||
r#"
|
||||
@@ -115,13 +110,7 @@ async fn list_messages(
|
||||
.bind(query.before)
|
||||
.bind(limit)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to list messages: {e}"),
|
||||
)
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let mut messages: Vec<Message> = messages
|
||||
.into_iter()
|
||||
@@ -149,18 +138,9 @@ async fn list_messages(
|
||||
.bind(user_id)
|
||||
.bind(room_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error updating membership timestamp: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to "))
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
tx.commit().await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not list messages".into(),
|
||||
)
|
||||
})?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(messages))
|
||||
}
|
||||
@@ -171,17 +151,14 @@ async fn create_message(
|
||||
Extension(realtime): Extension<RealtimeMessages>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<NewMessagePayload>,
|
||||
) -> Result<(StatusCode, Json<Message>), (StatusCode, String)> {
|
||||
) -> Result<(StatusCode, Json<Message>), 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::UNAUTHORIZED,
|
||||
String::from("You are not a member of this room"),
|
||||
));
|
||||
return Err(APIError::NotAMember);
|
||||
}
|
||||
|
||||
let uuid = Uuid::now_v7();
|
||||
@@ -196,8 +173,7 @@ async fn create_message(
|
||||
.bind(&payload.content)
|
||||
.bind(&uuid)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::BAD_REQUEST, "Could not create message".into()))?;
|
||||
.await?;
|
||||
|
||||
let sender_name = username_from_uuid(&db, claims.sub).await?;
|
||||
|
||||
@@ -221,11 +197,7 @@ async fn create_message(
|
||||
)
|
||||
.bind(room_id)
|
||||
.fetch_all(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error fetching message recipients: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "DB error".into())
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let rt = realtime.clone();
|
||||
let msg_clone = message.clone();
|
||||
@@ -244,7 +216,7 @@ async fn message_ws_handler(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
Extension(realtime): Extension<RealtimeMessages>,
|
||||
Extension(db): Extension<sqlx::PgPool>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
// tracing::info!("recieved ws handshake: {}", room_uuid);
|
||||
|
||||
let claims = verify_jwt_string(&query.token)?;
|
||||
@@ -260,13 +232,11 @@ async fn message_ws_handler(
|
||||
.bind(query.token)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to get WS token from DB: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "DB error".into())
|
||||
})?;
|
||||
// NOTE: Maybe wrong type of error
|
||||
.map_err(|e| APIError::Internal(format!("Failed to get WS token from DB: {e}")))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::UNAUTHORIZED, "Wrong token".into()));
|
||||
return Err(APIError::InvalidToken);
|
||||
}
|
||||
|
||||
let receiver = realtime.get_sender(user_uuid).subscribe();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,13 +12,12 @@ use uuid::Uuid;
|
||||
use validator::ValidateEmail;
|
||||
|
||||
use crate::{
|
||||
AppConfig,
|
||||
AppConfig, MAX_USERNAME_LENGTH,
|
||||
auth::{create_jwt, hash_password, validate_token, verify_jwt, verify_password},
|
||||
db::{user_id_from_uuid, username_from_uuid},
|
||||
errors::APIError,
|
||||
};
|
||||
|
||||
const DUMMY_HASH: &str = "$argon2id$v=19$m=4096,t=3,p=1$YWFhYWFhYWFhYWFhYWFhYQ$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
#[derive(sqlx::FromRow, serde::Serialize)]
|
||||
pub struct User {
|
||||
pub uuid: Uuid,
|
||||
@@ -92,14 +91,15 @@ async fn registration_guard(
|
||||
pub async fn login(
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<LoginPayload>,
|
||||
) -> Result<Json<LoginResponse>, (StatusCode, String)> {
|
||||
) -> Result<Json<LoginResponse>, APIError> {
|
||||
const DUMMY_HASH: &str = "$argon2id$v=19$m=4096,t=3,p=1$YWFhYWFhYWFhYWFhYWFhYQ$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
let user = sqlx::query_as::<_, User>(
|
||||
"SELECT uuid, email, username, password_hash FROM user_ WHERE email = $1",
|
||||
)
|
||||
.bind(&payload.email)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||
.await?;
|
||||
|
||||
let (user_uuid, password_hash) = if let Some(u) = user {
|
||||
(u.uuid, u.password_hash)
|
||||
@@ -109,10 +109,10 @@ pub async fn login(
|
||||
};
|
||||
|
||||
if !verify_password(&password_hash, &payload.password) {
|
||||
return Err((StatusCode::UNAUTHORIZED, "Invalid credentials".into()));
|
||||
return Err(APIError::WrongCredentials);
|
||||
}
|
||||
|
||||
let token = create_jwt(user_uuid).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
let token = create_jwt(user_uuid)?;
|
||||
let username = username_from_uuid(&db, user_uuid).await?;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
@@ -126,31 +126,27 @@ pub async fn login(
|
||||
pub async fn register_user(
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<NewUserPayload>,
|
||||
) -> Result<(StatusCode, Json<LoginResponse>), (StatusCode, String)> {
|
||||
) -> Result<(StatusCode, Json<LoginResponse>), APIError> {
|
||||
if payload.email.is_empty() || payload.username.is_empty() || payload.password.is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Cannot create a user with empty fields".into(),
|
||||
));
|
||||
return Err(APIError::EmptyFields);
|
||||
}
|
||||
|
||||
if !ValidateEmail::validate_email(&payload.email) {
|
||||
return Err((StatusCode::BAD_REQUEST, "Invalid email format".into()));
|
||||
return Err(APIError::InvalidEmail);
|
||||
}
|
||||
|
||||
{
|
||||
let username_length = payload.username.len();
|
||||
if username_length > MAX_USERNAME_LENGTH || username_length < 1 {
|
||||
return Err(APIError::UsernameLength);
|
||||
}
|
||||
}
|
||||
|
||||
if payload.password.len() < 8 {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Password must be at least 8 characters long".into(),
|
||||
));
|
||||
return Err(APIError::PasswordTooShort);
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&payload.password).map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password".into(),
|
||||
)
|
||||
})?;
|
||||
let password_hash = hash_password(&payload.password)?;
|
||||
|
||||
let user_uuid = uuid::Uuid::now_v7();
|
||||
|
||||
@@ -167,16 +163,17 @@ pub async fn register_user(
|
||||
.map_err(|e| {
|
||||
if let Some(db_err) = e.as_database_error() {
|
||||
if db_err.code().map(|c| c == "23505").unwrap_or(false) {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
"Email or username already taken".into(),
|
||||
);
|
||||
match db_err.constraint() {
|
||||
Some("user__username_key") => return APIError::UsernameTaken,
|
||||
Some("user__email_key") => return APIError::EmailTaken,
|
||||
_ => return APIError::Internal("".to_string()), // TODO: handle this case
|
||||
}
|
||||
}
|
||||
}
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
APIError::DatabaseError(e)
|
||||
})?;
|
||||
|
||||
let token = create_jwt(user_uuid).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
let token = create_jwt(user_uuid)?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
@@ -193,53 +190,33 @@ pub async fn update_user(
|
||||
headers: HeaderMap,
|
||||
Extension(db): Extension<PgPool>,
|
||||
Json(payload): Json<UpdateUserPayoad>,
|
||||
) -> Result<(StatusCode, Json<UpdateUserResponse>), (StatusCode, String)> {
|
||||
) -> Result<(StatusCode, Json<UpdateUserResponse>), APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
if payload.email.is_empty() || payload.username.is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Missing username or email fields".into(),
|
||||
));
|
||||
return Err(APIError::EmptyFields);
|
||||
}
|
||||
|
||||
if !ValidateEmail::validate_email(&payload.email) {
|
||||
return Err((StatusCode::BAD_REQUEST, "Invalid email format".into()));
|
||||
return Err(APIError::InvalidEmail);
|
||||
}
|
||||
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
|
||||
let mut tx = db
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB error".into()))?;
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if !payload.password.is_empty() {
|
||||
if payload.password.len() < 8 {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Password must be at least 8 characters long".into(),
|
||||
));
|
||||
return Err(APIError::PasswordTooShort);
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&payload.password).map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to hash password".into(),
|
||||
)
|
||||
})?;
|
||||
let password_hash = hash_password(&payload.password)?;
|
||||
|
||||
sqlx::query("UPDATE user_ SET password_hash = $1 WHERE id = $2")
|
||||
.bind(password_hash)
|
||||
.bind(user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to update password: {e}"),
|
||||
);
|
||||
})?;
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query("UPDATE user_ SET username = $1, email = $2 WHERE id = $3")
|
||||
@@ -251,21 +228,17 @@ pub async fn update_user(
|
||||
.map_err(|e| {
|
||||
if let Some(db_err) = e.as_database_error() {
|
||||
if db_err.code().map(|c| c == "23505").unwrap_or(false) {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
"Email or username already taken".into(),
|
||||
);
|
||||
match db_err.constraint() {
|
||||
Some("user__username_key") => return APIError::UsernameTaken,
|
||||
Some("user__email_key") => return APIError::EmailTaken,
|
||||
_ => return APIError::Internal("".to_string()), // TODO: handle this case
|
||||
}
|
||||
}
|
||||
}
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
APIError::DatabaseError(e)
|
||||
})?;
|
||||
|
||||
tx.commit().await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not update account".into(),
|
||||
)
|
||||
})?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
@@ -281,7 +254,7 @@ async fn upload_avatar(
|
||||
Extension(db): Extension<PgPool>,
|
||||
Extension(config): Extension<Arc<AppConfig>>,
|
||||
body: axum::body::Bytes,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
) -> Result<StatusCode, APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
let user_id = user_id_from_uuid(&db, claims.sub).await?;
|
||||
@@ -292,31 +265,19 @@ async fn upload_avatar(
|
||||
let filename = format!("{}.{}", claims.sub, file_extension);
|
||||
let full_path = std::path::Path::new(&base_dir).join(&filename);
|
||||
|
||||
tokio::fs::create_dir_all(&base_dir).await.map_err(|e| {
|
||||
tracing::error!("Failed to create storage: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to upload file".into(),
|
||||
)
|
||||
})?;
|
||||
tokio::fs::create_dir_all(&base_dir)
|
||||
.await
|
||||
.map_err(|e| APIError::Internal(format!("Failed to create storage: {e}")))?;
|
||||
|
||||
tokio::fs::write(&full_path, body).await.map_err(|e| {
|
||||
tracing::error!("Failed to save file: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to upload file".into(),
|
||||
)
|
||||
})?;
|
||||
tokio::fs::write(&full_path, body)
|
||||
.await
|
||||
.map_err(|e| APIError::Internal(format!("Failed to save file: {e}")))?;
|
||||
|
||||
sqlx::query("UPDATE user_ SET avatar_url = $1 WHERE id = $2")
|
||||
.bind(filename)
|
||||
.bind(user_id)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("DB error: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "DB erorr".into())
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -325,22 +286,18 @@ async fn upload_avatar(
|
||||
async fn get_avatar(
|
||||
Path(uuid): Path<Uuid>,
|
||||
Extension(config): Extension<Arc<AppConfig>>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
) -> Result<Response, APIError> {
|
||||
let base_dir = &config.avatar_dir;
|
||||
let filename = format!("{}.png", uuid);
|
||||
let full_path = std::path::Path::new(&base_dir).join(filename);
|
||||
|
||||
if !full_path.exists() {
|
||||
return Err((StatusCode::NOT_FOUND, "Avatar not found".into()));
|
||||
return Err(APIError::AvatarNotFound);
|
||||
}
|
||||
|
||||
let file_contents = tokio::fs::read(&full_path).await.map_err(|e| {
|
||||
tracing::error!("Could not read avatar file: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Could not read file".into(),
|
||||
)
|
||||
})?;
|
||||
let file_contents = tokio::fs::read(&full_path)
|
||||
.await
|
||||
.map_err(|e| APIError::Internal(format!("Could not read avatar file: {e}")))?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "image/png")
|
||||
|
||||
@@ -4,7 +4,6 @@ use axum::{
|
||||
ConnectInfo, Path, Query, WebSocketUpgrade,
|
||||
ws::{Message, WebSocket},
|
||||
},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
};
|
||||
@@ -17,9 +16,9 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
auth::verify_jwt_string,
|
||||
db::{room_id_from_uuid, user_id_from_uuid},
|
||||
errors::APIError,
|
||||
realtime::RealTimeVoices,
|
||||
routes::rooms::is_member,
|
||||
routes::ws::WsAuthQuery,
|
||||
routes::{rooms::is_member, ws::WsAuthQuery},
|
||||
};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
@@ -33,7 +32,7 @@ async fn voice_ws_handler(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
Extension(voice_manager): Extension<RealTimeVoices>,
|
||||
Extension(db): Extension<PgPool>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let claims = verify_jwt_string(&query.token)?;
|
||||
let user_uuid = claims.sub;
|
||||
|
||||
@@ -47,20 +46,18 @@ async fn voice_ws_handler(
|
||||
.bind(&query.token)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to get WS token from DB: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "DB error".into())
|
||||
})?;
|
||||
// NOTE: Maybe wrong type of error
|
||||
.map_err(|e| APIError::Internal(format!("Failed to get WS token from DB: {e}")))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::UNAUTHORIZED, "Invalid or expired token".into()));
|
||||
return Err(APIError::InvalidToken);
|
||||
}
|
||||
|
||||
let user_id = user_id_from_uuid(&db, user_uuid).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, "Not a member of this room".into()));
|
||||
return Err(APIError::NotAMember);
|
||||
}
|
||||
|
||||
tracing::info!("User {} joining voice in room {}", user_uuid, room_uuid);
|
||||
|
||||
@@ -5,6 +5,7 @@ use axum::{Extension, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::auth::{create_jwt, verify_jwt};
|
||||
use crate::errors::APIError;
|
||||
|
||||
#[derive(sqlx::FromRow, serde::Serialize, Deserialize)]
|
||||
pub struct WsAuthQuery {
|
||||
@@ -18,12 +19,12 @@ pub fn routes() -> axum::Router {
|
||||
pub async fn issue_ws_token(
|
||||
Extension(db): Extension<sqlx::PgPool>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<(StatusCode, Json<WsAuthQuery>), (StatusCode, String)> {
|
||||
) -> Result<(StatusCode, Json<WsAuthQuery>), APIError> {
|
||||
let claims = verify_jwt(headers)?;
|
||||
|
||||
tracing::debug!("Recieved token issue request from user {}", claims.sub);
|
||||
|
||||
let token = create_jwt(claims.sub).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
||||
let token = create_jwt(claims.sub)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -33,13 +34,7 @@ pub async fn issue_ws_token(
|
||||
)
|
||||
.bind(&token)
|
||||
.execute(&db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to provide ws token"),
|
||||
)
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(WsAuthQuery { token })))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user