From a9805b5eb18894fd802d9ac609dfd7e5b7d57fc3 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 15 Nov 2025 17:18:14 +0100 Subject: [PATCH] feat: Add public album routes and enhance authorization checks for media and albums --- libertas_api/src/handlers/media_handlers.rs | 20 ++++-- libertas_api/src/handlers/mod.rs | 5 +- libertas_api/src/handlers/public_handlers.rs | 33 ++++++++++ libertas_api/src/middleware/auth.rs | 29 +++++++++ libertas_api/src/routes.rs | 7 +- libertas_api/src/schema.rs | 28 ++++++-- libertas_api/src/services/album_service.rs | 29 +++++++-- .../src/services/authorization_service.rs | 65 +++++++++++++++---- libertas_api/src/services/media_service.rs | 6 +- libertas_api/src/services/person_service.rs | 16 ++--- libertas_api/src/services/tag_service.rs | 33 ++++++---- libertas_core/src/config.rs | 28 ++++---- libertas_core/src/models.rs | 5 ++ libertas_core/src/repositories.rs | 2 + libertas_core/src/services.rs | 64 ++++++++++++------ .../src/repositories/album_repository.rs | 45 ++++++++++++- 16 files changed, 323 insertions(+), 92 deletions(-) create mode 100644 libertas_api/src/handlers/public_handlers.rs diff --git a/libertas_api/src/handlers/media_handlers.rs b/libertas_api/src/handlers/media_handlers.rs index 16a0d78..e6f6eb3 100644 --- a/libertas_api/src/handlers/media_handlers.rs +++ b/libertas_api/src/handlers/media_handlers.rs @@ -13,7 +13,13 @@ use tower::ServiceExt; use tower_http::services::ServeFile; use uuid::Uuid; -use crate::{error::ApiError, extractors::query_options::ApiListMediaOptions, middleware::auth::UserId, schema::{MediaDetailsResponse, MediaMetadataResponse, MediaResponse}, state::AppState}; +use crate::{ + error::ApiError, + extractors::query_options::ApiListMediaOptions, + middleware::auth::{OptionalUserId, UserId}, + schema::{MediaDetailsResponse, MediaMetadataResponse, MediaResponse}, + state::AppState, +}; pub fn media_routes(max_upload_size: usize) -> Router { Router::new() @@ -62,7 +68,7 @@ async fn upload_media( async fn get_media_file( State(state): State, - UserId(user_id): UserId, + OptionalUserId(user_id): OptionalUserId, Path(media_id): Path, request: Request, ) -> Result { @@ -86,7 +92,7 @@ async fn get_media_file( async fn get_media_details( State(state): State, - UserId(user_id): UserId, + OptionalUserId(user_id): OptionalUserId, Path(id): Path, ) -> Result, ApiError> { let bundle = state.media_service.get_media_details(id, user_id).await?; @@ -97,13 +103,13 @@ async fn get_media_details( mime_type: bundle.media.mime_type, hash: bundle.media.hash, thumbnail_path: bundle.media.thumbnail_path, - metadata: bundle.metadata + metadata: bundle + .metadata .into_iter() .map(MediaMetadataResponse::from) .collect(), }; - Ok(Json(response)) } @@ -120,7 +126,7 @@ async fn list_user_media( State(state): State, UserId(user_id): UserId, ApiListMediaOptions(options): ApiListMediaOptions, -) -> Result>, ApiError> { +) -> Result>, ApiError> { let media_list = state .media_service .list_user_media(user_id, options) @@ -128,4 +134,4 @@ async fn list_user_media( let response = media_list.into_iter().map(MediaResponse::from).collect(); Ok(Json(response)) -} \ No newline at end of file +} diff --git a/libertas_api/src/handlers/mod.rs b/libertas_api/src/handlers/mod.rs index f859446..1314cbb 100644 --- a/libertas_api/src/handlers/mod.rs +++ b/libertas_api/src/handlers/mod.rs @@ -1,6 +1,7 @@ pub mod album_handlers; pub mod auth_handlers; pub mod media_handlers; -pub mod user_handlers; +pub mod person_handlers; +pub mod public_handlers; pub mod tag_handlers; -pub mod person_handlers; \ No newline at end of file +pub mod user_handlers; diff --git a/libertas_api/src/handlers/public_handlers.rs b/libertas_api/src/handlers/public_handlers.rs new file mode 100644 index 0000000..583df88 --- /dev/null +++ b/libertas_api/src/handlers/public_handlers.rs @@ -0,0 +1,33 @@ +use axum::{ + Json, Router, + extract::{Path, State}, + routing::get, +}; +use uuid::Uuid; + +use crate::{ + error::ApiError, + schema::{AlbumResponse, MediaResponse, PublicAlbumBundleResponse}, + state::AppState, +}; + +pub fn public_routes() -> Router { + Router::new().route("/public/albums/{id}", get(get_public_album)) +} + +async fn get_public_album( + State(state): State, + Path(album_id): Path, +) -> Result, ApiError> { + let bundle = state + .album_service + .get_public_album_bundle(album_id) + .await?; + + let response = PublicAlbumBundleResponse { + album: AlbumResponse::from(bundle.album), + media: bundle.media.into_iter().map(MediaResponse::from).collect(), + }; + + Ok(Json(response)) +} diff --git a/libertas_api/src/middleware/auth.rs b/libertas_api/src/middleware/auth.rs index bf301bd..d23fe48 100644 --- a/libertas_api/src/middleware/auth.rs +++ b/libertas_api/src/middleware/auth.rs @@ -14,6 +14,8 @@ use std::sync::Arc; pub struct UserId(pub Uuid); +pub struct OptionalUserId(pub Option); + impl FromRequestParts for UserId { type Rejection = Response; @@ -47,3 +49,30 @@ impl FromRequestParts for UserId { } } } + +impl FromRequestParts for OptionalUserId { + type Rejection = Response; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + let tokenizer: Arc = state.token_generator.clone(); + + let result = (async || -> CoreResult { + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|_| CoreError::Auth("Missing Authorization header".to_string()))?; + + let user_id = tokenizer.verify_token(bearer.token())?; + Ok(user_id) + })() + .await; + + match result { + Ok(user_id) => Ok(Self(Some(user_id))), + Err(_) => Ok(Self(None)), + } + } +} diff --git a/libertas_api/src/routes.rs b/libertas_api/src/routes.rs index 66dccc4..3bc9e82 100644 --- a/libertas_api/src/routes.rs +++ b/libertas_api/src/routes.rs @@ -1,7 +1,10 @@ use axum::{Router, routing::get}; use crate::{ - handlers::{album_handlers, auth_handlers, media_handlers, person_handlers, tag_handlers, user_handlers}, + handlers::{ + album_handlers, auth_handlers, media_handlers, person_handlers, public_handlers, + tag_handlers, user_handlers, + }, state::AppState, }; @@ -13,9 +16,11 @@ pub fn api_routes(max_upload_size: usize) -> Router { let media_tag_routes = tag_handlers::tag_routes(); let people_routes = person_handlers::people_routes(); let face_routes = person_handlers::face_routes(); + let public_routes = public_handlers::public_routes(); Router::new() .route("/api/v1/health", get(|| async { "OK" })) + .nest("/api/v1", public_routes) .nest("/api/v1/auth", auth_routes) .nest("/api/v1/users", user_routes) .nest("/api/v1/media", media_routes) diff --git a/libertas_api/src/schema.rs b/libertas_api/src/schema.rs index 0b4d82b..f017db6 100644 --- a/libertas_api/src/schema.rs +++ b/libertas_api/src/schema.rs @@ -1,4 +1,6 @@ -use libertas_core::models::{Album, AlbumPermission, FaceRegion, Media, MediaMetadata, Person, PersonPermission, Tag}; +use libertas_core::models::{ + Album, AlbumPermission, FaceRegion, Media, MediaMetadata, Person, PersonPermission, Tag, +}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -20,7 +22,7 @@ impl From for MediaResponse { original_filename: media.original_filename, mime_type: media.mime_type, hash: media.hash, - thumbnail_path: media.thumbnail_path, + thumbnail_path: media.thumbnail_path, } } } @@ -31,7 +33,8 @@ pub struct ListMediaParams { pub order: Option, pub mime_type: Option, #[serde(default)] - pub metadata: Vec,} + pub metadata: Vec, +} #[derive(Deserialize)] pub struct CreateAlbumRequest { @@ -144,7 +147,10 @@ pub struct TagResponse { impl From for TagResponse { fn from(tag: Tag) -> Self { - Self { id: tag.id, name: tag.name } + Self { + id: tag.id, + name: tag.name, + } } } @@ -162,7 +168,11 @@ pub struct PersonResponse { impl From for PersonResponse { fn from(person: Person) -> Self { - Self { id: person.id, owner_id: person.owner_id, name: person.name } + Self { + id: person.id, + owner_id: person.owner_id, + name: person.name, + } } } @@ -210,4 +220,10 @@ pub struct AssignFaceRequest { pub struct SharePersonRequest { pub target_user_id: Uuid, pub permission: PersonPermission, -} \ No newline at end of file +} + +#[derive(Serialize)] +pub struct PublicAlbumBundleResponse { + pub album: AlbumResponse, + pub media: Vec, +} diff --git a/libertas_api/src/services/album_service.rs b/libertas_api/src/services/album_service.rs index 79243fd..3f5df9c 100644 --- a/libertas_api/src/services/album_service.rs +++ b/libertas_api/src/services/album_service.rs @@ -5,7 +5,7 @@ use chrono::Utc; use libertas_core::{ authz::{self, Permission}, error::{CoreError, CoreResult}, - models::Album, + models::{Album, PublicAlbumBundle}, repositories::{AlbumRepository, AlbumShareRepository}, schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData}, services::{AlbumService, AuthorizationService}, @@ -57,7 +57,7 @@ impl AlbumService for AlbumServiceImpl { async fn get_album_details(&self, album_id: Uuid, user_id: Uuid) -> CoreResult { self.auth_service - .check_permission(user_id, Permission::ViewAlbum(album_id)) + .check_permission(Some(user_id), Permission::ViewAlbum(album_id)) .await?; let album = self @@ -71,12 +71,12 @@ impl AlbumService for AlbumServiceImpl { async fn add_media_to_album(&self, data: AddMediaToAlbumData, user_id: Uuid) -> CoreResult<()> { self.auth_service - .check_permission(user_id, Permission::AddToAlbum(data.album_id)) + .check_permission(Some(user_id), Permission::AddToAlbum(data.album_id)) .await?; for media_id in &data.media_ids { self.auth_service - .check_permission(*media_id, Permission::ViewMedia(*media_id)) + .check_permission(Some(user_id), Permission::ViewMedia(*media_id)) .await?; } @@ -91,7 +91,7 @@ impl AlbumService for AlbumServiceImpl { async fn share_album(&self, data: ShareAlbumData, owner_id: Uuid) -> CoreResult<()> { self.auth_service - .check_permission(owner_id, Permission::ShareAlbum(data.album_id)) + .check_permission(Some(owner_id), Permission::ShareAlbum(data.album_id)) .await?; if data.target_user_id == owner_id { @@ -112,7 +112,7 @@ impl AlbumService for AlbumServiceImpl { data: UpdateAlbumData<'_>, ) -> CoreResult { self.auth_service - .check_permission(user_id, Permission::EditAlbum(album_id)) + .check_permission(Some(user_id), Permission::EditAlbum(album_id)) .await?; let mut album = self @@ -150,9 +150,24 @@ impl AlbumService for AlbumServiceImpl { async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()> { self.auth_service - .check_permission(user_id, Permission::DeleteAlbum(album_id)) + .check_permission(Some(user_id), Permission::DeleteAlbum(album_id)) .await?; self.album_repo.delete(album_id).await } + + async fn get_public_album_bundle(&self, album_id: Uuid) -> CoreResult { + let album = self + .album_repo + .find_by_id(album_id) + .await? + .ok_or(CoreError::NotFound("Album".to_string(), album_id))?; + + if !album.is_public { + return Err(CoreError::Auth("Album is not public".to_string())); + } + + let media = self.album_repo.list_media_by_album_id(album_id).await?; + Ok(PublicAlbumBundle { album, media }) + } } diff --git a/libertas_api/src/services/authorization_service.rs b/libertas_api/src/services/authorization_service.rs index c9d1a62..6813507 100644 --- a/libertas_api/src/services/authorization_service.rs +++ b/libertas_api/src/services/authorization_service.rs @@ -107,27 +107,46 @@ impl AuthorizationServiceImpl { #[async_trait] impl AuthorizationService for AuthorizationServiceImpl { - async fn check_permission(&self, user_id: Uuid, permission: Permission) -> CoreResult<()> { - let user = self.get_user(user_id).await?; + async fn check_permission( + &self, + user_id: Option, + permission: Permission, + ) -> CoreResult<()> { + let user = if let Some(id) = user_id { + Some(self.get_user(id).await?) + } else { + None + }; - if authz::is_admin(&user) { - return Ok(()); + if let Some(ref user) = user { + if authz::is_admin(user) { + // [cite: 115] + return Ok(()); + } } match permission { Permission::ViewMedia(media_id) => { let media = self.get_media(media_id).await?; - if authz::is_owner(user_id, &media) { + + if self.album_repo.is_media_in_public_album(media_id).await? { return Ok(()); } - let is_shared = self - .album_share_repo - .is_media_in_shared_album(media_id, user_id) - .await?; + if let Some(id) = user_id { + if authz::is_owner(id, &media) { + // [cite: 117] + return Ok(()); + } - if is_shared { - return Ok(()); + if self + .album_share_repo + .is_media_in_shared_album(media_id, id) + .await? + { + // [cite: 118-119] + return Ok(()); + } } Err(CoreError::Auth( @@ -136,6 +155,9 @@ impl AuthorizationService for AuthorizationServiceImpl { } Permission::DeleteMedia(media_id) | Permission::EditMedia(media_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let media = self.get_media(media_id).await?; if authz::is_owner(user_id, &media) { return Ok(()); @@ -149,6 +171,9 @@ impl AuthorizationService for AuthorizationServiceImpl { Permission::AddTags(media_id) | Permission::RemoveTags(media_id) | Permission::EditTags(media_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let media = self.get_media(media_id).await?; if authz::is_owner(user_id, &media) { @@ -170,6 +195,9 @@ impl AuthorizationService for AuthorizationServiceImpl { } Permission::ViewAlbum(album_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let album = self.get_album(album_id).await?; let share_permission = self.get_album_share_permission(album_id, user_id).await?; @@ -184,6 +212,9 @@ impl AuthorizationService for AuthorizationServiceImpl { } Permission::AddToAlbum(album_id) | Permission::EditAlbum(album_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let album = self.get_album(album_id).await?; let share_permission = self.get_album_share_permission(album_id, user_id).await?; @@ -197,6 +228,9 @@ impl AuthorizationService for AuthorizationServiceImpl { } Permission::ShareAlbum(album_id) | Permission::DeleteAlbum(album_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let album = self.get_album(album_id).await?; if authz::is_owner(user_id, &album) { @@ -209,6 +243,9 @@ impl AuthorizationService for AuthorizationServiceImpl { } Permission::ViewPerson(person_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let person = self.get_person(person_id).await?; let share_permission = self.get_person_share_permission(person_id, user_id).await?; @@ -224,6 +261,9 @@ impl AuthorizationService for AuthorizationServiceImpl { Permission::EditPerson(person_id) | Permission::SharePerson(person_id) | Permission::DeletePerson(person_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let person = self.get_person(person_id).await?; if authz::is_owner(user_id, &person) { @@ -236,6 +276,9 @@ impl AuthorizationService for AuthorizationServiceImpl { } Permission::UsePerson(person_id) => { + let user_id = user_id.ok_or(CoreError::Auth( + "Authentication required for this action".into(), + ))?; let person = self.get_person(person_id).await?; let share_permission = self.get_person_share_permission(person_id, user_id).await?; diff --git a/libertas_api/src/services/media_service.rs b/libertas_api/src/services/media_service.rs index e78bf5f..2b2cba6 100644 --- a/libertas_api/src/services/media_service.rs +++ b/libertas_api/src/services/media_service.rs @@ -89,7 +89,7 @@ impl MediaService for MediaServiceImpl { Ok(media) } - async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult { + async fn get_media_details(&self, id: Uuid, user_id: Option) -> CoreResult { self.auth_service .check_permission(user_id, authz::Permission::ViewMedia(id)) .await?; @@ -113,7 +113,7 @@ impl MediaService for MediaServiceImpl { self.repo.list_by_user(user_id, &options).await } - async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult { + async fn get_media_filepath(&self, id: Uuid, user_id: Option) -> CoreResult { self.auth_service .check_permission(user_id, authz::Permission::ViewMedia(id)) .await?; @@ -129,7 +129,7 @@ impl MediaService for MediaServiceImpl { async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> { self.auth_service - .check_permission(user_id, authz::Permission::DeleteMedia(id)) + .check_permission(Some(user_id), authz::Permission::DeleteMedia(id)) .await?; let media = self diff --git a/libertas_api/src/services/person_service.rs b/libertas_api/src/services/person_service.rs index 93daafd..e248d43 100644 --- a/libertas_api/src/services/person_service.rs +++ b/libertas_api/src/services/person_service.rs @@ -59,7 +59,7 @@ impl PersonService for PersonServiceImpl { async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult { self.auth_service - .check_permission(user_id, authz::Permission::ViewPerson(person_id)) + .check_permission(Some(user_id), authz::Permission::ViewPerson(person_id)) .await?; self.person_repo @@ -93,7 +93,7 @@ impl PersonService for PersonServiceImpl { user_id: Uuid, ) -> CoreResult { self.auth_service - .check_permission(user_id, authz::Permission::EditPerson(person_id)) + .check_permission(Some(user_id), authz::Permission::EditPerson(person_id)) .await?; let mut person = self.get_person(person_id).await?; @@ -105,7 +105,7 @@ impl PersonService for PersonServiceImpl { async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()> { self.auth_service - .check_permission(user_id, authz::Permission::DeletePerson(person_id)) + .check_permission(Some(user_id), authz::Permission::DeletePerson(person_id)) .await?; self.person_repo.delete(person_id).await @@ -118,10 +118,10 @@ impl PersonService for PersonServiceImpl { user_id: Uuid, ) -> CoreResult { self.auth_service - .check_permission(user_id, authz::Permission::UsePerson(person_id)) + .check_permission(Some(user_id), authz::Permission::UsePerson(person_id)) .await?; self.auth_service - .check_permission(user_id, authz::Permission::AssignFace(face_region_id)) + .check_permission(Some(user_id), authz::Permission::AssignFace(face_region_id)) .await?; let mut face = @@ -147,7 +147,7 @@ impl PersonService for PersonServiceImpl { user_id: Uuid, ) -> CoreResult> { self.auth_service - .check_permission(user_id, authz::Permission::ViewFaces(media_id)) + .check_permission(Some(user_id), authz::Permission::ViewFaces(media_id)) .await?; self.face_repo.find_by_media_id(media_id).await @@ -161,7 +161,7 @@ impl PersonService for PersonServiceImpl { owner_id: Uuid, ) -> CoreResult<()> { self.auth_service - .check_permission(owner_id, authz::Permission::SharePerson(person_id)) + .check_permission(Some(owner_id), authz::Permission::SharePerson(person_id)) .await?; self.person_share_repo @@ -176,7 +176,7 @@ impl PersonService for PersonServiceImpl { owner_id: Uuid, ) -> CoreResult<()> { self.auth_service - .check_permission(owner_id, authz::Permission::SharePerson(person_id)) + .check_permission(Some(owner_id), authz::Permission::SharePerson(person_id)) .await?; self.person_share_repo diff --git a/libertas_api/src/services/tag_service.rs b/libertas_api/src/services/tag_service.rs index 98deabb..6b740a1 100644 --- a/libertas_api/src/services/tag_service.rs +++ b/libertas_api/src/services/tag_service.rs @@ -1,7 +1,13 @@ use std::sync::Arc; use async_trait::async_trait; -use libertas_core::{authz::Permission, error::CoreResult, models::Tag, repositories::TagRepository, services::{AuthorizationService, TagService}}; +use libertas_core::{ + authz::Permission, + error::CoreResult, + models::Tag, + repositories::TagRepository, + services::{AuthorizationService, TagService}, +}; use uuid::Uuid; pub struct TagServiceImpl { @@ -28,9 +34,10 @@ impl TagService for TagServiceImpl { media_id: Uuid, tag_names: &[String], user_id: Uuid, - ) -> CoreResult> { - self.auth_service.check_permission(user_id, Permission::AddTags(media_id)).await?; + self.auth_service + .check_permission(Some(user_id), Permission::AddTags(media_id)) + .await?; let mut tag_ids = Vec::new(); let tags = self.tag_repo.find_or_create_tags(tag_names).await?; @@ -49,7 +56,9 @@ impl TagService for TagServiceImpl { tag_names: &[String], user_id: Uuid, ) -> CoreResult<()> { - self.auth_service.check_permission(user_id, Permission::RemoveTags(media_id)).await?; + self.auth_service + .check_permission(Some(user_id), Permission::RemoveTags(media_id)) + .await?; let tags = self.tag_repo.find_or_create_tags(tag_names).await?; let mut tag_ids = Vec::new(); @@ -57,20 +66,20 @@ impl TagService for TagServiceImpl { tag_ids.push(tag.id); } - self.tag_repo.remove_tags_from_media(media_id, &tag_ids).await?; + self.tag_repo + .remove_tags_from_media(media_id, &tag_ids) + .await?; Ok(()) } - async fn list_tags_for_media( - &self, - media_id: Uuid, - user_id: Uuid, - ) -> CoreResult> { - self.auth_service.check_permission(user_id, Permission::ViewMedia(media_id)).await?; + async fn list_tags_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult> { + self.auth_service + .check_permission(Some(user_id), Permission::ViewMedia(media_id)) + .await?; let tags = self.tag_repo.list_tags_for_media(media_id).await?; Ok(tags) } -} \ No newline at end of file +} diff --git a/libertas_core/src/config.rs b/libertas_core/src/config.rs index d7a7755..51625ad 100644 --- a/libertas_core/src/config.rs +++ b/libertas_core/src/config.rs @@ -15,7 +15,6 @@ pub struct DatabaseConfig { pub url: String, } - #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum ThumbnailFormat { @@ -53,8 +52,12 @@ pub struct Config { pub thumbnail_config: Option, } -fn default_max_upload_size() -> u32 { 100 } -fn default_storage_quota() -> u64 { 10 } +fn default_max_upload_size() -> u32 { + 100 +} +fn default_storage_quota() -> u64 { + 10 +} fn default_allowed_sort_columns() -> Vec { vec!["created_at".to_string(), "original_filename".to_string()] } @@ -78,20 +81,19 @@ pub fn load_config() -> CoreResult { println!("Loaded config from {}", env_path.display()); let config = config::Config::builder() - .add_source(config::Environment::default() - .with_list_parse_key("allowed_sort_columns") - .list_separator(",") - .try_parsing(true) - .separator("__") - ) + .add_source( + config::Environment::default() + .with_list_parse_key("allowed_sort_columns") + .list_separator(",") + .try_parsing(true) + .separator("__"), + ) .build() .map_err(|e| CoreError::Config(format!("Failed to build config: {}", e)))?; - + let config: Config = config .try_deserialize() .map_err(|e| CoreError::Config(format!("Failed to deserialize config: {}", e)))?; - println!("Built config from environment variables. Contents: {:?}", config); - Ok(AppConfig { database: DatabaseConfig { @@ -107,4 +109,4 @@ pub fn load_config() -> CoreResult { allowed_sort_columns: Some(config.allowed_sort_columns), thumbnail_config: config.thumbnail_config, }) -} \ No newline at end of file +} diff --git a/libertas_core/src/models.rs b/libertas_core/src/models.rs index 7f17324..0cb75d2 100644 --- a/libertas_core/src/models.rs +++ b/libertas_core/src/models.rs @@ -149,6 +149,11 @@ pub struct AlbumShare { pub permission: AlbumPermission, } +pub struct PublicAlbumBundle { + pub album: Album, + pub media: Vec, +} + pub struct MediaBundle { pub media: Media, pub metadata: Vec, diff --git a/libertas_core/src/repositories.rs b/libertas_core/src/repositories.rs index 07d986c..36500ff 100644 --- a/libertas_core/src/repositories.rs +++ b/libertas_core/src/repositories.rs @@ -41,6 +41,8 @@ pub trait AlbumRepository: Send + Sync { async fn add_media_to_album(&self, album_id: Uuid, media_ids: &[Uuid]) -> CoreResult<()>; async fn update(&self, album: Album) -> CoreResult<()>; async fn delete(&self, id: Uuid) -> CoreResult<()>; + async fn list_media_by_album_id(&self, album_id: Uuid) -> CoreResult>; + async fn is_media_in_public_album(&self, media_id: Uuid) -> CoreResult; } #[async_trait] diff --git a/libertas_core/src/services.rs b/libertas_core/src/services.rs index 792815b..bec7ce5 100644 --- a/libertas_core/src/services.rs +++ b/libertas_core/src/services.rs @@ -2,17 +2,28 @@ use async_trait::async_trait; use uuid::Uuid; use crate::{ - authz::Permission, error::CoreResult, models::{Album, FaceRegion, Media, MediaBundle, Person, PersonPermission, Tag, User}, schema::{ - AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData, ShareAlbumData, UpdateAlbumData, UploadMediaData - } + authz::Permission, + error::CoreResult, + models::{ + Album, FaceRegion, Media, MediaBundle, Person, PersonPermission, PublicAlbumBundle, Tag, + User, + }, + schema::{ + AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData, + ShareAlbumData, UpdateAlbumData, UploadMediaData, + }, }; #[async_trait] pub trait MediaService: Send + Sync { async fn upload_media(&self, data: UploadMediaData<'_>) -> CoreResult; - async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult; - async fn list_user_media(&self, user_id: Uuid, options: ListMediaOptions) -> CoreResult>; - async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult; + async fn get_media_details(&self, id: Uuid, user_id: Option) -> CoreResult; + async fn list_user_media( + &self, + user_id: Uuid, + options: ListMediaOptions, + ) -> CoreResult>; + async fn get_media_filepath(&self, id: Uuid, user_id: Option) -> CoreResult; async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>; } @@ -37,12 +48,23 @@ pub trait AlbumService: Send + Sync { data: UpdateAlbumData<'_>, ) -> CoreResult; async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()>; + async fn get_public_album_bundle(&self, album_id: Uuid) -> CoreResult; } #[async_trait] pub trait TagService: Send + Sync { - async fn add_tags_to_media(&self, media_id: Uuid, tag_names: &[String], user_id: Uuid) -> CoreResult>; - async fn remove_tags_from_media(&self, media_id: Uuid, tag_names: &[String], user_id: Uuid) -> CoreResult<()>; + async fn add_tags_to_media( + &self, + media_id: Uuid, + tag_names: &[String], + user_id: Uuid, + ) -> CoreResult>; + async fn remove_tags_from_media( + &self, + media_id: Uuid, + tag_names: &[String], + user_id: Uuid, + ) -> CoreResult<()>; async fn list_tags_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult>; } @@ -51,22 +73,22 @@ pub trait PersonService: Send + Sync { async fn create_person(&self, name: &str, owner_id: Uuid) -> CoreResult; async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult; async fn list_people(&self, user_id: Uuid) -> CoreResult>; - async fn update_person( - &self, - person_id: Uuid, - name: &str, - user_id: Uuid, - ) -> CoreResult; + async fn update_person(&self, person_id: Uuid, name: &str, user_id: Uuid) + -> CoreResult; async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()>; - + async fn assign_face_to_person( &self, face_region_id: Uuid, person_id: Uuid, user_id: Uuid, ) -> CoreResult; - - async fn list_faces_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult>; + + async fn list_faces_for_media( + &self, + media_id: Uuid, + user_id: Uuid, + ) -> CoreResult>; async fn share_person( &self, @@ -86,5 +108,9 @@ pub trait PersonService: Send + Sync { #[async_trait] pub trait AuthorizationService: Send + Sync { - async fn check_permission(&self, user_id: Uuid, permission: Permission) -> CoreResult<()>; -} \ No newline at end of file + async fn check_permission( + &self, + user_id: Option, + permission: Permission, + ) -> CoreResult<()>; +} diff --git a/libertas_infra/src/repositories/album_repository.rs b/libertas_infra/src/repositories/album_repository.rs index 7816002..f42a1e6 100644 --- a/libertas_infra/src/repositories/album_repository.rs +++ b/libertas_infra/src/repositories/album_repository.rs @@ -1,13 +1,13 @@ use async_trait::async_trait; use libertas_core::{ error::{CoreError, CoreResult}, - models::Album, + models::{Album, Media}, repositories::AlbumRepository, }; use sqlx::PgPool; use uuid::Uuid; -use crate::db_models::PostgresAlbum; +use crate::db_models::{PostgresAlbum, PostgresMedia}; #[derive(Clone)] pub struct PostgresAlbumRepository { @@ -72,7 +72,7 @@ impl AlbumRepository for PostgresAlbumRepository { .fetch_all(&self.pool) .await .map_err(|e| CoreError::Database(e.to_string()))?; - + Ok(pg_albums.into_iter().map(|a| a.into()).collect()) } @@ -127,4 +127,43 @@ impl AlbumRepository for PostgresAlbumRepository { Ok(()) } + + async fn list_media_by_album_id(&self, album_id: Uuid) -> CoreResult> { + let pg_media = sqlx::query_as!( + PostgresMedia, + r#" + SELECT m.id, m.owner_id, m.storage_path, m.original_filename, m.mime_type, + m.hash, m.created_at, m.thumbnail_path + FROM media m + JOIN album_media am ON m.id = am.media_id + WHERE am.album_id = $1 + "#, + album_id + ) + .fetch_all(&self.pool) + .await + .map_err(|e| CoreError::Database(e.to_string()))?; + + let media_list = pg_media.into_iter().map(|m| m.into()).collect(); + Ok(media_list) + } + + async fn is_media_in_public_album(&self, media_id: Uuid) -> CoreResult { + let result = sqlx::query!( + r#" + SELECT EXISTS ( + SELECT 1 + FROM album_media am + JOIN albums a ON am.album_id = a.id + WHERE am.media_id = $1 AND a.is_public = TRUE + ) as "exists!" + "#, + media_id + ) + .fetch_one(&self.pool) + .await + .map_err(|e| CoreError::Database(e.to_string()))?; + + Ok(result.exists) + } }