feat: enhance album and media management with update and delete functionalities
This commit is contained in:
@@ -2,12 +2,13 @@ use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
routing::{get, post},
|
||||
};
|
||||
use libertas_core::{
|
||||
models::AlbumPermission,
|
||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData},
|
||||
models::{Album, AlbumPermission},
|
||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
|
||||
@@ -68,7 +69,7 @@ pub struct ShareAlbumRequest {
|
||||
|
||||
async fn share_album(
|
||||
State(state): State<AppState>,
|
||||
UserId(owner_id): UserId, // The person sharing must be authenticated
|
||||
UserId(owner_id): UserId,
|
||||
Path(album_id): Path<Uuid>,
|
||||
Json(payload): Json<ShareAlbumRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
@@ -83,9 +84,95 @@ async fn share_album(
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AlbumResponse {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
is_public: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<Album> for AlbumResponse {
|
||||
fn from(album: Album) -> Self {
|
||||
Self {
|
||||
id: album.id,
|
||||
owner_id: album.owner_id,
|
||||
name: album.name,
|
||||
description: album.description,
|
||||
is_public: album.is_public,
|
||||
created_at: album.created_at,
|
||||
updated_at: album.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateAlbumRequest {
|
||||
name: Option<String>,
|
||||
description: Option<Option<String>>,
|
||||
is_public: Option<bool>,
|
||||
}
|
||||
|
||||
async fn list_user_albums(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
) -> Result<Json<Vec<AlbumResponse>>, ApiError> {
|
||||
let albums = state.album_service.list_user_albums(user_id).await?;
|
||||
let response = albums.into_iter().map(AlbumResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn get_album_details(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(album_id): Path<Uuid>,
|
||||
) -> Result<Json<AlbumResponse>, ApiError> {
|
||||
let album = state
|
||||
.album_service
|
||||
.get_album_details(album_id, user_id)
|
||||
.await?;
|
||||
Ok(Json(album.into()))
|
||||
}
|
||||
|
||||
async fn update_album(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(album_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateAlbumRequest>,
|
||||
) -> Result<Json<AlbumResponse>, ApiError> {
|
||||
let data = UpdateAlbumData {
|
||||
name: payload.name.as_deref(),
|
||||
description: payload.description.as_ref().map(|opt_s| opt_s.as_deref()),
|
||||
is_public: payload.is_public,
|
||||
};
|
||||
let album = state
|
||||
.album_service
|
||||
.update_album(album_id, user_id, data)
|
||||
.await?;
|
||||
Ok(Json(album.into()))
|
||||
}
|
||||
|
||||
async fn delete_album(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(album_id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
state.album_service.delete_album(album_id, user_id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub fn album_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::post(create_album))
|
||||
.route("/{album_id}/media", axum::routing::post(add_media_to_album))
|
||||
.route("/{album_id}/share", axum::routing::post(share_album))
|
||||
.route("/", post(create_album).get(list_user_albums))
|
||||
.route(
|
||||
"/{id}",
|
||||
get(get_album_details)
|
||||
.put(update_album)
|
||||
.delete(delete_album),
|
||||
)
|
||||
.route("/{id}/media", post(add_media_to_album))
|
||||
.route("/{id}/share", post(share_album))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use libertas_core::schema::{CreateUserData, LoginUserData};
|
||||
use libertas_core::schema::{CreateUserData, LoginUserData, UserResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
@@ -12,13 +11,6 @@ pub struct RegisterRequest {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserResponse {
|
||||
id: Uuid,
|
||||
username: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
@@ -64,16 +56,8 @@ pub async fn login(
|
||||
Ok(Json(LoginResponse { token }))
|
||||
}
|
||||
|
||||
pub async fn get_me(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user = state.user_service.get_user_details(user_id).await?;
|
||||
|
||||
let response = UserResponse {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
};
|
||||
Ok(Json(response))
|
||||
pub fn auth_routes() -> axum::Router<AppState> {
|
||||
axum::Router::new()
|
||||
.route("/register", axum::routing::post(register))
|
||||
.route("/login", axum::routing::post(login))
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ impl From<Media> for MediaResponse {
|
||||
pub fn media_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(upload_media))
|
||||
.route("/{media_id}/file", get(get_media_file))
|
||||
.route("/{id}", get(get_media_details).delete(delete_media))
|
||||
.route("/{id}/file", get(get_media_file))
|
||||
.layer(DefaultBodyLimit::max(250 * 1024 * 1024))
|
||||
}
|
||||
|
||||
@@ -104,3 +105,21 @@ async fn get_media_file(
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_media_details(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<MediaResponse>, ApiError> {
|
||||
let media = state.media_service.get_media_details(id, user_id).await?;
|
||||
Ok(Json(media.into()))
|
||||
}
|
||||
|
||||
async fn delete_media(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
state.media_service.delete_media(id, user_id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod album_handlers;
|
||||
pub mod auth_handlers;
|
||||
pub mod media_handlers;
|
||||
pub mod user_handlers;
|
||||
|
||||
22
libertas_api/src/handlers/user_handlers.rs
Normal file
22
libertas_api/src/handlers/user_handlers.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use axum::{Json, Router, extract::State};
|
||||
use libertas_core::schema::UserResponse;
|
||||
|
||||
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
|
||||
|
||||
pub async fn get_me(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
) -> Result<Json<UserResponse>, ApiError> {
|
||||
let user = state.user_service.get_user_details(user_id).await?;
|
||||
|
||||
let response = UserResponse {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
};
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
pub fn user_routes() -> Router<AppState> {
|
||||
Router::new().route("/me", axum::routing::get(get_me))
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::{
|
||||
handlers::{album_handlers, auth_handlers, media_handlers},
|
||||
handlers::{album_handlers, auth_handlers, media_handlers, user_handlers},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub fn api_routes() -> Router<AppState> {
|
||||
let auth_routes = Router::new()
|
||||
.route("/register", post(auth_handlers::register))
|
||||
.route("/login", post(auth_handlers::login));
|
||||
|
||||
let user_routes = Router::new().route("/me", get(auth_handlers::get_me));
|
||||
let auth_routes = auth_handlers::auth_routes();
|
||||
let user_routes = user_handlers::user_routes();
|
||||
let media_routes = media_handlers::media_routes();
|
||||
let album_routes = album_handlers::album_routes();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
models::Album,
|
||||
repositories::{AlbumRepository, AlbumShareRepository, MediaRepository},
|
||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData},
|
||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData},
|
||||
services::AlbumService,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
@@ -139,4 +139,70 @@ impl AlbumService for AlbumServiceImpl {
|
||||
.create_or_update_share(data.album_id, data.target_user_id, data.permission)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_album(
|
||||
&self,
|
||||
album_id: Uuid,
|
||||
user_id: Uuid,
|
||||
data: UpdateAlbumData<'_>,
|
||||
) -> CoreResult<Album> {
|
||||
let mut album = self
|
||||
.album_repo
|
||||
.find_by_id(album_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Album".to_string(), album_id))?;
|
||||
|
||||
let share_permission = self
|
||||
.album_share_repo
|
||||
.get_user_permission(album_id, user_id)
|
||||
.await?;
|
||||
|
||||
if !authz::can_contribute_to_album(user_id, &album, share_permission) {
|
||||
return Err(CoreError::Auth(
|
||||
"User does not have permission to update this album".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(name) = data.name {
|
||||
if name.is_empty() {
|
||||
return Err(CoreError::Validation(
|
||||
"Album name cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
album.name = name.to_string();
|
||||
}
|
||||
|
||||
if let Some(description) = data.description {
|
||||
album.description = description.map(|s| s.to_string());
|
||||
}
|
||||
|
||||
if let Some(is_public) = data.is_public {
|
||||
if !authz::is_owner(user_id, &album) && is_public {
|
||||
return Err(CoreError::Auth(
|
||||
"Only the owner can make an album public".to_string(),
|
||||
));
|
||||
}
|
||||
album.is_public = is_public;
|
||||
}
|
||||
|
||||
self.album_repo.update(album.clone()).await?;
|
||||
|
||||
Ok(album)
|
||||
}
|
||||
|
||||
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||
let album = self
|
||||
.album_repo
|
||||
.find_by_id(album_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Album".to_string(), album_id))?;
|
||||
|
||||
if !authz::is_owner(user_id, &album) {
|
||||
return Err(CoreError::Auth(
|
||||
"Only the album owner can delete the album".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.album_repo.delete(album_id).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,4 +192,46 @@ impl MediaService for MediaServiceImpl {
|
||||
|
||||
Err(CoreError::Auth("Access denied".to_string()))
|
||||
}
|
||||
|
||||
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||
let media = self
|
||||
.repo
|
||||
.find_by_id(id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Media".to_string(), id))?;
|
||||
|
||||
let user = self
|
||||
.user_repo
|
||||
.find_by_id(user_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("User".to_string(), user_id))?;
|
||||
|
||||
if !authz::is_owner(user_id, &media) && !authz::is_admin(&user) {
|
||||
return Err(CoreError::Auth("Access denied".to_string()));
|
||||
}
|
||||
|
||||
let full_path = PathBuf::from(&self.config.media_library_path).join(&media.storage_path);
|
||||
self.repo.delete(id).await?;
|
||||
|
||||
let file_size = match fs::metadata(&full_path).await {
|
||||
Ok(metadata) => metadata.len() as i64,
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
if let Err(e) = fs::remove_file(full_path).await {
|
||||
tracing::error!("Failed to delete media file from disk: {}", e);
|
||||
}
|
||||
|
||||
self.user_repo
|
||||
.update_storage_used(user.id, -file_size)
|
||||
.await?;
|
||||
|
||||
let job_payload = json!({ "media_id": id });
|
||||
self.nats_client
|
||||
.publish("media.deleted".to_string(), job_payload.to_string().into())
|
||||
.await
|
||||
.map_err(|e| CoreError::Unknown(format!("Failed to publish NATS job: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user