feat: Add public album routes and enhance authorization checks for media and albums
This commit is contained in:
@@ -13,7 +13,13 @@ use tower::ServiceExt;
|
|||||||
use tower_http::services::ServeFile;
|
use tower_http::services::ServeFile;
|
||||||
use uuid::Uuid;
|
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<AppState> {
|
pub fn media_routes(max_upload_size: usize) -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -62,7 +68,7 @@ async fn upload_media(
|
|||||||
|
|
||||||
async fn get_media_file(
|
async fn get_media_file(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
UserId(user_id): UserId,
|
OptionalUserId(user_id): OptionalUserId,
|
||||||
Path(media_id): Path<Uuid>,
|
Path(media_id): Path<Uuid>,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
@@ -86,7 +92,7 @@ async fn get_media_file(
|
|||||||
|
|
||||||
async fn get_media_details(
|
async fn get_media_details(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
UserId(user_id): UserId,
|
OptionalUserId(user_id): OptionalUserId,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<Json<MediaDetailsResponse>, ApiError> {
|
) -> Result<Json<MediaDetailsResponse>, ApiError> {
|
||||||
let bundle = state.media_service.get_media_details(id, user_id).await?;
|
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,
|
mime_type: bundle.media.mime_type,
|
||||||
hash: bundle.media.hash,
|
hash: bundle.media.hash,
|
||||||
thumbnail_path: bundle.media.thumbnail_path,
|
thumbnail_path: bundle.media.thumbnail_path,
|
||||||
metadata: bundle.metadata
|
metadata: bundle
|
||||||
|
.metadata
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(MediaMetadataResponse::from)
|
.map(MediaMetadataResponse::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod album_handlers;
|
pub mod album_handlers;
|
||||||
pub mod auth_handlers;
|
pub mod auth_handlers;
|
||||||
pub mod media_handlers;
|
pub mod media_handlers;
|
||||||
pub mod user_handlers;
|
|
||||||
pub mod tag_handlers;
|
|
||||||
pub mod person_handlers;
|
pub mod person_handlers;
|
||||||
|
pub mod public_handlers;
|
||||||
|
pub mod tag_handlers;
|
||||||
|
pub mod user_handlers;
|
||||||
|
|||||||
33
libertas_api/src/handlers/public_handlers.rs
Normal file
33
libertas_api/src/handlers/public_handlers.rs
Normal file
@@ -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<AppState> {
|
||||||
|
Router::new().route("/public/albums/{id}", get(get_public_album))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_public_album(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(album_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<PublicAlbumBundleResponse>, 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))
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
pub struct UserId(pub Uuid);
|
pub struct UserId(pub Uuid);
|
||||||
|
|
||||||
|
pub struct OptionalUserId(pub Option<Uuid>);
|
||||||
|
|
||||||
impl FromRequestParts<AppState> for UserId {
|
impl FromRequestParts<AppState> for UserId {
|
||||||
type Rejection = Response;
|
type Rejection = Response;
|
||||||
|
|
||||||
@@ -47,3 +49,30 @@ impl FromRequestParts<AppState> for UserId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromRequestParts<AppState> for OptionalUserId {
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
parts: &mut Parts,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let tokenizer: Arc<dyn TokenGenerator> = state.token_generator.clone();
|
||||||
|
|
||||||
|
let result = (async || -> CoreResult<Uuid> {
|
||||||
|
let TypedHeader(Authorization(bearer)) = parts
|
||||||
|
.extract::<TypedHeader<Authorization<Bearer>>>()
|
||||||
|
.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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use axum::{Router, routing::get};
|
use axum::{Router, routing::get};
|
||||||
|
|
||||||
use crate::{
|
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,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,9 +16,11 @@ pub fn api_routes(max_upload_size: usize) -> Router<AppState> {
|
|||||||
let media_tag_routes = tag_handlers::tag_routes();
|
let media_tag_routes = tag_handlers::tag_routes();
|
||||||
let people_routes = person_handlers::people_routes();
|
let people_routes = person_handlers::people_routes();
|
||||||
let face_routes = person_handlers::face_routes();
|
let face_routes = person_handlers::face_routes();
|
||||||
|
let public_routes = public_handlers::public_routes();
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/health", get(|| async { "OK" }))
|
.route("/api/v1/health", get(|| async { "OK" }))
|
||||||
|
.nest("/api/v1", public_routes)
|
||||||
.nest("/api/v1/auth", auth_routes)
|
.nest("/api/v1/auth", auth_routes)
|
||||||
.nest("/api/v1/users", user_routes)
|
.nest("/api/v1/users", user_routes)
|
||||||
.nest("/api/v1/media", media_routes)
|
.nest("/api/v1/media", media_routes)
|
||||||
|
|||||||
@@ -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 serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -31,7 +33,8 @@ pub struct ListMediaParams {
|
|||||||
pub order: Option<String>,
|
pub order: Option<String>,
|
||||||
pub mime_type: Option<String>,
|
pub mime_type: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub metadata: Vec<String>,}
|
pub metadata: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateAlbumRequest {
|
pub struct CreateAlbumRequest {
|
||||||
@@ -144,7 +147,10 @@ pub struct TagResponse {
|
|||||||
|
|
||||||
impl From<Tag> for TagResponse {
|
impl From<Tag> for TagResponse {
|
||||||
fn from(tag: Tag) -> Self {
|
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<Person> for PersonResponse {
|
impl From<Person> for PersonResponse {
|
||||||
fn from(person: Person) -> Self {
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,3 +221,9 @@ pub struct SharePersonRequest {
|
|||||||
pub target_user_id: Uuid,
|
pub target_user_id: Uuid,
|
||||||
pub permission: PersonPermission,
|
pub permission: PersonPermission,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PublicAlbumBundleResponse {
|
||||||
|
pub album: AlbumResponse,
|
||||||
|
pub media: Vec<MediaResponse>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use chrono::Utc;
|
|||||||
use libertas_core::{
|
use libertas_core::{
|
||||||
authz::{self, Permission},
|
authz::{self, Permission},
|
||||||
error::{CoreError, CoreResult},
|
error::{CoreError, CoreResult},
|
||||||
models::Album,
|
models::{Album, PublicAlbumBundle},
|
||||||
repositories::{AlbumRepository, AlbumShareRepository},
|
repositories::{AlbumRepository, AlbumShareRepository},
|
||||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData},
|
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData},
|
||||||
services::{AlbumService, AuthorizationService},
|
services::{AlbumService, AuthorizationService},
|
||||||
@@ -57,7 +57,7 @@ impl AlbumService for AlbumServiceImpl {
|
|||||||
|
|
||||||
async fn get_album_details(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<Album> {
|
async fn get_album_details(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<Album> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, Permission::ViewAlbum(album_id))
|
.check_permission(Some(user_id), Permission::ViewAlbum(album_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let album = self
|
let album = self
|
||||||
@@ -71,12 +71,12 @@ impl AlbumService for AlbumServiceImpl {
|
|||||||
|
|
||||||
async fn add_media_to_album(&self, data: AddMediaToAlbumData, user_id: Uuid) -> CoreResult<()> {
|
async fn add_media_to_album(&self, data: AddMediaToAlbumData, user_id: Uuid) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, Permission::AddToAlbum(data.album_id))
|
.check_permission(Some(user_id), Permission::AddToAlbum(data.album_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for media_id in &data.media_ids {
|
for media_id in &data.media_ids {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(*media_id, Permission::ViewMedia(*media_id))
|
.check_permission(Some(user_id), Permission::ViewMedia(*media_id))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ impl AlbumService for AlbumServiceImpl {
|
|||||||
|
|
||||||
async fn share_album(&self, data: ShareAlbumData, owner_id: Uuid) -> CoreResult<()> {
|
async fn share_album(&self, data: ShareAlbumData, owner_id: Uuid) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(owner_id, Permission::ShareAlbum(data.album_id))
|
.check_permission(Some(owner_id), Permission::ShareAlbum(data.album_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if data.target_user_id == owner_id {
|
if data.target_user_id == owner_id {
|
||||||
@@ -112,7 +112,7 @@ impl AlbumService for AlbumServiceImpl {
|
|||||||
data: UpdateAlbumData<'_>,
|
data: UpdateAlbumData<'_>,
|
||||||
) -> CoreResult<Album> {
|
) -> CoreResult<Album> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, Permission::EditAlbum(album_id))
|
.check_permission(Some(user_id), Permission::EditAlbum(album_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut album = self
|
let mut album = self
|
||||||
@@ -150,9 +150,24 @@ impl AlbumService for AlbumServiceImpl {
|
|||||||
|
|
||||||
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, Permission::DeleteAlbum(album_id))
|
.check_permission(Some(user_id), Permission::DeleteAlbum(album_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.album_repo.delete(album_id).await
|
self.album_repo.delete(album_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_public_album_bundle(&self, album_id: Uuid) -> CoreResult<PublicAlbumBundle> {
|
||||||
|
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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,27 +107,46 @@ impl AuthorizationServiceImpl {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AuthorizationService for AuthorizationServiceImpl {
|
impl AuthorizationService for AuthorizationServiceImpl {
|
||||||
async fn check_permission(&self, user_id: Uuid, permission: Permission) -> CoreResult<()> {
|
async fn check_permission(
|
||||||
let user = self.get_user(user_id).await?;
|
&self,
|
||||||
|
user_id: Option<Uuid>,
|
||||||
|
permission: Permission,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
let user = if let Some(id) = user_id {
|
||||||
|
Some(self.get_user(id).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if authz::is_admin(&user) {
|
if let Some(ref user) = user {
|
||||||
return Ok(());
|
if authz::is_admin(user) {
|
||||||
|
// [cite: 115]
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match permission {
|
match permission {
|
||||||
Permission::ViewMedia(media_id) => {
|
Permission::ViewMedia(media_id) => {
|
||||||
let media = self.get_media(media_id).await?;
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_shared = self
|
if let Some(id) = user_id {
|
||||||
.album_share_repo
|
if authz::is_owner(id, &media) {
|
||||||
.is_media_in_shared_album(media_id, user_id)
|
// [cite: 117]
|
||||||
.await?;
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if is_shared {
|
if self
|
||||||
return Ok(());
|
.album_share_repo
|
||||||
|
.is_media_in_shared_album(media_id, id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
// [cite: 118-119]
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(CoreError::Auth(
|
Err(CoreError::Auth(
|
||||||
@@ -136,6 +155,9 @@ impl AuthorizationService for AuthorizationServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Permission::DeleteMedia(media_id) | Permission::EditMedia(media_id) => {
|
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?;
|
let media = self.get_media(media_id).await?;
|
||||||
if authz::is_owner(user_id, &media) {
|
if authz::is_owner(user_id, &media) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -149,6 +171,9 @@ impl AuthorizationService for AuthorizationServiceImpl {
|
|||||||
Permission::AddTags(media_id)
|
Permission::AddTags(media_id)
|
||||||
| Permission::RemoveTags(media_id)
|
| Permission::RemoveTags(media_id)
|
||||||
| Permission::EditTags(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?;
|
let media = self.get_media(media_id).await?;
|
||||||
|
|
||||||
if authz::is_owner(user_id, &media) {
|
if authz::is_owner(user_id, &media) {
|
||||||
@@ -170,6 +195,9 @@ impl AuthorizationService for AuthorizationServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Permission::ViewAlbum(album_id) => {
|
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 album = self.get_album(album_id).await?;
|
||||||
|
|
||||||
let share_permission = self.get_album_share_permission(album_id, user_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) => {
|
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 album = self.get_album(album_id).await?;
|
||||||
let share_permission = self.get_album_share_permission(album_id, user_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) => {
|
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?;
|
let album = self.get_album(album_id).await?;
|
||||||
|
|
||||||
if authz::is_owner(user_id, &album) {
|
if authz::is_owner(user_id, &album) {
|
||||||
@@ -209,6 +243,9 @@ impl AuthorizationService for AuthorizationServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Permission::ViewPerson(person_id) => {
|
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 person = self.get_person(person_id).await?;
|
||||||
let share_permission = self.get_person_share_permission(person_id, user_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::EditPerson(person_id)
|
||||||
| Permission::SharePerson(person_id)
|
| Permission::SharePerson(person_id)
|
||||||
| Permission::DeletePerson(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?;
|
let person = self.get_person(person_id).await?;
|
||||||
|
|
||||||
if authz::is_owner(user_id, &person) {
|
if authz::is_owner(user_id, &person) {
|
||||||
@@ -236,6 +276,9 @@ impl AuthorizationService for AuthorizationServiceImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Permission::UsePerson(person_id) => {
|
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 person = self.get_person(person_id).await?;
|
||||||
let share_permission = self.get_person_share_permission(person_id, user_id).await?;
|
let share_permission = self.get_person_share_permission(person_id, user_id).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl MediaService for MediaServiceImpl {
|
|||||||
Ok(media)
|
Ok(media)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult<MediaBundle> {
|
async fn get_media_details(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<MediaBundle> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::ViewMedia(id))
|
.check_permission(user_id, authz::Permission::ViewMedia(id))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -113,7 +113,7 @@ impl MediaService for MediaServiceImpl {
|
|||||||
self.repo.list_by_user(user_id, &options).await
|
self.repo.list_by_user(user_id, &options).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult<String> {
|
async fn get_media_filepath(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<String> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::ViewMedia(id))
|
.check_permission(user_id, authz::Permission::ViewMedia(id))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -129,7 +129,7 @@ impl MediaService for MediaServiceImpl {
|
|||||||
|
|
||||||
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::DeleteMedia(id))
|
.check_permission(Some(user_id), authz::Permission::DeleteMedia(id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let media = self
|
let media = self
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ impl PersonService for PersonServiceImpl {
|
|||||||
|
|
||||||
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person> {
|
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::ViewPerson(person_id))
|
.check_permission(Some(user_id), authz::Permission::ViewPerson(person_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.person_repo
|
self.person_repo
|
||||||
@@ -93,7 +93,7 @@ impl PersonService for PersonServiceImpl {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> CoreResult<Person> {
|
) -> CoreResult<Person> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::EditPerson(person_id))
|
.check_permission(Some(user_id), authz::Permission::EditPerson(person_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut person = self.get_person(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<()> {
|
async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::DeletePerson(person_id))
|
.check_permission(Some(user_id), authz::Permission::DeletePerson(person_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.person_repo.delete(person_id).await
|
self.person_repo.delete(person_id).await
|
||||||
@@ -118,10 +118,10 @@ impl PersonService for PersonServiceImpl {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> CoreResult<FaceRegion> {
|
) -> CoreResult<FaceRegion> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::UsePerson(person_id))
|
.check_permission(Some(user_id), authz::Permission::UsePerson(person_id))
|
||||||
.await?;
|
.await?;
|
||||||
self.auth_service
|
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?;
|
.await?;
|
||||||
|
|
||||||
let mut face =
|
let mut face =
|
||||||
@@ -147,7 +147,7 @@ impl PersonService for PersonServiceImpl {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> CoreResult<Vec<FaceRegion>> {
|
) -> CoreResult<Vec<FaceRegion>> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(user_id, authz::Permission::ViewFaces(media_id))
|
.check_permission(Some(user_id), authz::Permission::ViewFaces(media_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.face_repo.find_by_media_id(media_id).await
|
self.face_repo.find_by_media_id(media_id).await
|
||||||
@@ -161,7 +161,7 @@ impl PersonService for PersonServiceImpl {
|
|||||||
owner_id: Uuid,
|
owner_id: Uuid,
|
||||||
) -> CoreResult<()> {
|
) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(owner_id, authz::Permission::SharePerson(person_id))
|
.check_permission(Some(owner_id), authz::Permission::SharePerson(person_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.person_share_repo
|
self.person_share_repo
|
||||||
@@ -176,7 +176,7 @@ impl PersonService for PersonServiceImpl {
|
|||||||
owner_id: Uuid,
|
owner_id: Uuid,
|
||||||
) -> CoreResult<()> {
|
) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(owner_id, authz::Permission::SharePerson(person_id))
|
.check_permission(Some(owner_id), authz::Permission::SharePerson(person_id))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.person_share_repo
|
self.person_share_repo
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
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;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct TagServiceImpl {
|
pub struct TagServiceImpl {
|
||||||
@@ -28,9 +34,10 @@ impl TagService for TagServiceImpl {
|
|||||||
media_id: Uuid,
|
media_id: Uuid,
|
||||||
tag_names: &[String],
|
tag_names: &[String],
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
|
|
||||||
) -> CoreResult<Vec<Tag>> {
|
) -> CoreResult<Vec<Tag>> {
|
||||||
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 mut tag_ids = Vec::new();
|
||||||
let tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
let tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
||||||
@@ -49,7 +56,9 @@ impl TagService for TagServiceImpl {
|
|||||||
tag_names: &[String],
|
tag_names: &[String],
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> CoreResult<()> {
|
) -> 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 tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
||||||
let mut tag_ids = Vec::new();
|
let mut tag_ids = Vec::new();
|
||||||
@@ -57,17 +66,17 @@ impl TagService for TagServiceImpl {
|
|||||||
tag_ids.push(tag.id);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_tags_for_media(
|
async fn list_tags_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult<Vec<Tag>> {
|
||||||
&self,
|
self.auth_service
|
||||||
media_id: Uuid,
|
.check_permission(Some(user_id), Permission::ViewMedia(media_id))
|
||||||
user_id: Uuid,
|
.await?;
|
||||||
) -> CoreResult<Vec<Tag>> {
|
|
||||||
self.auth_service.check_permission(user_id, Permission::ViewMedia(media_id)).await?;
|
|
||||||
|
|
||||||
let tags = self.tag_repo.list_tags_for_media(media_id).await?;
|
let tags = self.tag_repo.list_tags_for_media(media_id).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ pub struct DatabaseConfig {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ThumbnailFormat {
|
pub enum ThumbnailFormat {
|
||||||
@@ -53,8 +52,12 @@ pub struct Config {
|
|||||||
pub thumbnail_config: Option<ThumbnailConfig>,
|
pub thumbnail_config: Option<ThumbnailConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_upload_size() -> u32 { 100 }
|
fn default_max_upload_size() -> u32 {
|
||||||
fn default_storage_quota() -> u64 { 10 }
|
100
|
||||||
|
}
|
||||||
|
fn default_storage_quota() -> u64 {
|
||||||
|
10
|
||||||
|
}
|
||||||
fn default_allowed_sort_columns() -> Vec<String> {
|
fn default_allowed_sort_columns() -> Vec<String> {
|
||||||
vec!["created_at".to_string(), "original_filename".to_string()]
|
vec!["created_at".to_string(), "original_filename".to_string()]
|
||||||
}
|
}
|
||||||
@@ -78,20 +81,19 @@ pub fn load_config() -> CoreResult<AppConfig> {
|
|||||||
println!("Loaded config from {}", env_path.display());
|
println!("Loaded config from {}", env_path.display());
|
||||||
|
|
||||||
let config = config::Config::builder()
|
let config = config::Config::builder()
|
||||||
.add_source(config::Environment::default()
|
.add_source(
|
||||||
.with_list_parse_key("allowed_sort_columns")
|
config::Environment::default()
|
||||||
.list_separator(",")
|
.with_list_parse_key("allowed_sort_columns")
|
||||||
.try_parsing(true)
|
.list_separator(",")
|
||||||
.separator("__")
|
.try_parsing(true)
|
||||||
)
|
.separator("__"),
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| CoreError::Config(format!("Failed to build config: {}", e)))?;
|
.map_err(|e| CoreError::Config(format!("Failed to build config: {}", e)))?;
|
||||||
|
|
||||||
let config: Config = config
|
let config: Config = config
|
||||||
.try_deserialize()
|
.try_deserialize()
|
||||||
.map_err(|e| CoreError::Config(format!("Failed to deserialize config: {}", e)))?;
|
.map_err(|e| CoreError::Config(format!("Failed to deserialize config: {}", e)))?;
|
||||||
println!("Built config from environment variables. Contents: {:?}", config);
|
|
||||||
|
|
||||||
|
|
||||||
Ok(AppConfig {
|
Ok(AppConfig {
|
||||||
database: DatabaseConfig {
|
database: DatabaseConfig {
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ pub struct AlbumShare {
|
|||||||
pub permission: AlbumPermission,
|
pub permission: AlbumPermission,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PublicAlbumBundle {
|
||||||
|
pub album: Album,
|
||||||
|
pub media: Vec<Media>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MediaBundle {
|
pub struct MediaBundle {
|
||||||
pub media: Media,
|
pub media: Media,
|
||||||
pub metadata: Vec<MediaMetadata>,
|
pub metadata: Vec<MediaMetadata>,
|
||||||
|
|||||||
@@ -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 add_media_to_album(&self, album_id: Uuid, media_ids: &[Uuid]) -> CoreResult<()>;
|
||||||
async fn update(&self, album: Album) -> CoreResult<()>;
|
async fn update(&self, album: Album) -> CoreResult<()>;
|
||||||
async fn delete(&self, id: Uuid) -> CoreResult<()>;
|
async fn delete(&self, id: Uuid) -> CoreResult<()>;
|
||||||
|
async fn list_media_by_album_id(&self, album_id: Uuid) -> CoreResult<Vec<Media>>;
|
||||||
|
async fn is_media_in_public_album(&self, media_id: Uuid) -> CoreResult<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -2,17 +2,28 @@ use async_trait::async_trait;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
authz::Permission, error::CoreResult, models::{Album, FaceRegion, Media, MediaBundle, Person, PersonPermission, Tag, User}, schema::{
|
authz::Permission,
|
||||||
AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData, ShareAlbumData, UpdateAlbumData, UploadMediaData
|
error::CoreResult,
|
||||||
}
|
models::{
|
||||||
|
Album, FaceRegion, Media, MediaBundle, Person, PersonPermission, PublicAlbumBundle, Tag,
|
||||||
|
User,
|
||||||
|
},
|
||||||
|
schema::{
|
||||||
|
AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData,
|
||||||
|
ShareAlbumData, UpdateAlbumData, UploadMediaData,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MediaService: Send + Sync {
|
pub trait MediaService: Send + Sync {
|
||||||
async fn upload_media(&self, data: UploadMediaData<'_>) -> CoreResult<Media>;
|
async fn upload_media(&self, data: UploadMediaData<'_>) -> CoreResult<Media>;
|
||||||
async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult<MediaBundle>;
|
async fn get_media_details(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<MediaBundle>;
|
||||||
async fn list_user_media(&self, user_id: Uuid, options: ListMediaOptions) -> CoreResult<Vec<Media>>;
|
async fn list_user_media(
|
||||||
async fn get_media_filepath(&self, id: Uuid, user_id: Uuid) -> CoreResult<String>;
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
options: ListMediaOptions,
|
||||||
|
) -> CoreResult<Vec<Media>>;
|
||||||
|
async fn get_media_filepath(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<String>;
|
||||||
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,12 +48,23 @@ pub trait AlbumService: Send + Sync {
|
|||||||
data: UpdateAlbumData<'_>,
|
data: UpdateAlbumData<'_>,
|
||||||
) -> CoreResult<Album>;
|
) -> CoreResult<Album>;
|
||||||
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
|
async fn get_public_album_bundle(&self, album_id: Uuid) -> CoreResult<PublicAlbumBundle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TagService: Send + Sync {
|
pub trait TagService: Send + Sync {
|
||||||
async fn add_tags_to_media(&self, media_id: Uuid, tag_names: &[String], user_id: Uuid) -> CoreResult<Vec<Tag>>;
|
async fn add_tags_to_media(
|
||||||
async fn remove_tags_from_media(&self, media_id: Uuid, tag_names: &[String], user_id: Uuid) -> CoreResult<()>;
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
tag_names: &[String],
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Vec<Tag>>;
|
||||||
|
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<Vec<Tag>>;
|
async fn list_tags_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult<Vec<Tag>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +73,8 @@ pub trait PersonService: Send + Sync {
|
|||||||
async fn create_person(&self, name: &str, owner_id: Uuid) -> CoreResult<Person>;
|
async fn create_person(&self, name: &str, owner_id: Uuid) -> CoreResult<Person>;
|
||||||
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person>;
|
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person>;
|
||||||
async fn list_people(&self, user_id: Uuid) -> CoreResult<Vec<Person>>;
|
async fn list_people(&self, user_id: Uuid) -> CoreResult<Vec<Person>>;
|
||||||
async fn update_person(
|
async fn update_person(&self, person_id: Uuid, name: &str, user_id: Uuid)
|
||||||
&self,
|
-> CoreResult<Person>;
|
||||||
person_id: Uuid,
|
|
||||||
name: &str,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> CoreResult<Person>;
|
|
||||||
async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
|
|
||||||
async fn assign_face_to_person(
|
async fn assign_face_to_person(
|
||||||
@@ -66,7 +84,11 @@ pub trait PersonService: Send + Sync {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> CoreResult<FaceRegion>;
|
) -> CoreResult<FaceRegion>;
|
||||||
|
|
||||||
async fn list_faces_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult<Vec<FaceRegion>>;
|
async fn list_faces_for_media(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Vec<FaceRegion>>;
|
||||||
|
|
||||||
async fn share_person(
|
async fn share_person(
|
||||||
&self,
|
&self,
|
||||||
@@ -86,5 +108,9 @@ pub trait PersonService: Send + Sync {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AuthorizationService: Send + Sync {
|
pub trait AuthorizationService: Send + Sync {
|
||||||
async fn check_permission(&self, user_id: Uuid, permission: Permission) -> CoreResult<()>;
|
async fn check_permission(
|
||||||
|
&self,
|
||||||
|
user_id: Option<Uuid>,
|
||||||
|
permission: Permission,
|
||||||
|
) -> CoreResult<()>;
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use libertas_core::{
|
use libertas_core::{
|
||||||
error::{CoreError, CoreResult},
|
error::{CoreError, CoreResult},
|
||||||
models::Album,
|
models::{Album, Media},
|
||||||
repositories::AlbumRepository,
|
repositories::AlbumRepository,
|
||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::db_models::PostgresAlbum;
|
use crate::db_models::{PostgresAlbum, PostgresMedia};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PostgresAlbumRepository {
|
pub struct PostgresAlbumRepository {
|
||||||
@@ -127,4 +127,43 @@ impl AlbumRepository for PostgresAlbumRepository {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_media_by_album_id(&self, album_id: Uuid) -> CoreResult<Vec<Media>> {
|
||||||
|
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<bool> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user