feat: Implement person and tag management services
- Added `Person` and `Tag` models to the core library. - Created `PersonService` and `TagService` traits with implementations for managing persons and tags. - Introduced repositories for `Person`, `Tag`, `FaceRegion`, and `PersonShare` with PostgreSQL support. - Updated authorization logic to include permissions for accessing and editing persons. - Enhanced the schema to support new models and relationships. - Implemented database migrations for new tables related to persons and tags. - Added request and response structures for API interactions with persons and tags.
This commit is contained in:
@@ -5,14 +5,13 @@ use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
};
|
||||
use libertas_infra::factory::{
|
||||
build_album_repository, build_album_share_repository, build_database_pool, build_media_metadata_repository, build_media_repository, build_user_repository
|
||||
build_album_repository, build_album_share_repository, build_database_pool, build_face_region_repository, build_media_metadata_repository, build_media_repository, build_person_repository, build_person_share_repository, build_tag_repository, build_user_repository
|
||||
};
|
||||
|
||||
use crate::{
|
||||
security::{Argon2Hasher, JwtGenerator},
|
||||
services::{
|
||||
album_service::AlbumServiceImpl, media_service::MediaServiceImpl,
|
||||
user_service::UserServiceImpl,
|
||||
album_service::AlbumServiceImpl, media_service::MediaServiceImpl, person_service::PersonServiceImpl, tag_service::TagServiceImpl, user_service::UserServiceImpl
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -31,6 +30,10 @@ pub async fn build_app_state(config: AppConfig) -> CoreResult<AppState> {
|
||||
let album_share_repo = build_album_share_repository(&config.database, db_pool.clone()).await?;
|
||||
let media_metadata_repo =
|
||||
build_media_metadata_repository(&config.database, db_pool.clone()).await?;
|
||||
let tag_repo = build_tag_repository(&config.database, db_pool.clone()).await?;
|
||||
let person_repo = build_person_repository(&config.database, db_pool.clone()).await?;
|
||||
let face_region_repo = build_face_region_repository(&config.database, db_pool.clone()).await?;
|
||||
let person_share_repo = build_person_share_repository(&config.database, db_pool.clone()).await?;
|
||||
|
||||
let hasher = Arc::new(Argon2Hasher::default());
|
||||
let tokenizer = Arc::new(JwtGenerator::new(config.jwt_secret.clone()));
|
||||
@@ -51,14 +54,26 @@ pub async fn build_app_state(config: AppConfig) -> CoreResult<AppState> {
|
||||
));
|
||||
let album_service = Arc::new(AlbumServiceImpl::new(
|
||||
album_repo,
|
||||
media_repo,
|
||||
media_repo.clone(),
|
||||
album_share_repo,
|
||||
));
|
||||
let tag_service = Arc::new(TagServiceImpl::new(
|
||||
tag_repo,
|
||||
media_repo.clone(),
|
||||
));
|
||||
let person_service = Arc::new(PersonServiceImpl::new(
|
||||
person_repo,
|
||||
face_region_repo,
|
||||
media_repo.clone(),
|
||||
person_share_repo,
|
||||
));
|
||||
|
||||
Ok(AppState {
|
||||
user_service,
|
||||
media_service,
|
||||
album_service,
|
||||
tag_service,
|
||||
person_service,
|
||||
token_generator: tokenizer,
|
||||
nats_client,
|
||||
config,
|
||||
|
||||
@@ -2,3 +2,5 @@ pub mod album_handlers;
|
||||
pub mod auth_handlers;
|
||||
pub mod media_handlers;
|
||||
pub mod user_handlers;
|
||||
pub mod tag_handlers;
|
||||
pub mod person_handlers;
|
||||
152
libertas_api/src/handlers/person_handlers.rs
Normal file
152
libertas_api/src/handlers/person_handlers.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
middleware::auth::UserId,
|
||||
schema::{
|
||||
AssignFaceRequest, CreatePersonRequest, FaceRegionResponse, PersonResponse,
|
||||
SharePersonRequest, UpdatePersonRequest,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub fn people_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_people).post(create_person))
|
||||
.route(
|
||||
"/{person_id}",
|
||||
get(get_person)
|
||||
.put(update_person)
|
||||
.delete(delete_person),
|
||||
)
|
||||
.route("/{person_id}/share", post(share_person).delete(unshare_person))
|
||||
}
|
||||
|
||||
pub fn face_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/media/{media_id}/faces", get(list_faces_for_media))
|
||||
.route("/faces/{face_id}/person", put(assign_face_to_person))
|
||||
}
|
||||
|
||||
async fn create_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Json(payload): Json<CreatePersonRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let person = state
|
||||
.person_service
|
||||
.create_person(&payload.name, user_id)
|
||||
.await?;
|
||||
Ok((StatusCode::CREATED, Json(PersonResponse::from(person))))
|
||||
}
|
||||
|
||||
async fn get_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(person_id): Path<Uuid>,
|
||||
) -> Result<Json<PersonResponse>, ApiError> {
|
||||
let person = state
|
||||
.person_service
|
||||
.get_person(person_id, user_id)
|
||||
.await?;
|
||||
Ok(Json(PersonResponse::from(person)))
|
||||
}
|
||||
|
||||
async fn list_people(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
) -> Result<Json<Vec<PersonResponse>>, ApiError> {
|
||||
let people = state.person_service.list_people(user_id).await?;
|
||||
let response = people.into_iter().map(PersonResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn update_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(person_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdatePersonRequest>,
|
||||
) -> Result<Json<PersonResponse>, ApiError> {
|
||||
let person = state
|
||||
.person_service
|
||||
.update_person(person_id, &payload.name, user_id)
|
||||
.await?;
|
||||
Ok(Json(PersonResponse::from(person)))
|
||||
}
|
||||
|
||||
async fn delete_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(person_id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
state
|
||||
.person_service
|
||||
.delete_person(person_id, user_id)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn share_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(person_id): Path<Uuid>,
|
||||
Json(payload): Json<SharePersonRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
state
|
||||
.person_service
|
||||
.share_person(
|
||||
person_id,
|
||||
payload.target_user_id,
|
||||
payload.permission,
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
async fn unshare_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(person_id): Path<Uuid>,
|
||||
Json(payload): Json<SharePersonRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
state
|
||||
.person_service
|
||||
.unshare_person(person_id, payload.target_user_id, user_id)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn list_faces_for_media(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(media_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<FaceRegionResponse>>, ApiError> {
|
||||
let faces = state
|
||||
.person_service
|
||||
.list_faces_for_media(media_id, user_id)
|
||||
.await?;
|
||||
let response = faces.into_iter().map(FaceRegionResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn assign_face_to_person(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(face_id): Path<Uuid>,
|
||||
Json(payload): Json<AssignFaceRequest>,
|
||||
) -> Result<Json<FaceRegionResponse>, ApiError> {
|
||||
let face = state
|
||||
.person_service
|
||||
.assign_face_to_person(face_id, payload.person_id, user_id)
|
||||
.await?;
|
||||
Ok(Json(FaceRegionResponse::from(face)))
|
||||
}
|
||||
53
libertas_api/src/handlers/tag_handlers.rs
Normal file
53
libertas_api/src/handlers/tag_handlers.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use axum::{Json, Router, extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{delete, post}};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{error::ApiError, middleware::auth::UserId, schema::{MediaTagsRequest, TagResponse}, state::AppState};
|
||||
|
||||
pub fn tag_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(add_tags_to_media).get(list_tags_for_media))
|
||||
.route(
|
||||
"/{tag_name}",
|
||||
delete(remove_tag_from_media),
|
||||
)
|
||||
}
|
||||
|
||||
async fn add_tags_to_media(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(media_id): Path<Uuid>,
|
||||
Json(payload): Json<MediaTagsRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let tags = state
|
||||
.tag_service
|
||||
.add_tags_to_media(media_id, &payload.tags, user_id)
|
||||
.await?;
|
||||
|
||||
let response: Vec<TagResponse> = tags.into_iter().map(TagResponse::from).collect();
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
}
|
||||
|
||||
async fn list_tags_for_media(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path(media_id): Path<Uuid>,
|
||||
) -> Result<Json<Vec<TagResponse>>, ApiError> {
|
||||
let tags = state
|
||||
.tag_service
|
||||
.list_tags_for_media(media_id, user_id)
|
||||
.await?;
|
||||
let response = tags.into_iter().map(TagResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn remove_tag_from_media(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Path((media_id, tag_name)): Path<(Uuid, String)>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
state
|
||||
.tag_service
|
||||
.remove_tags_from_media(media_id, &[tag_name], user_id)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::{
|
||||
handlers::{album_handlers, auth_handlers, media_handlers, user_handlers},
|
||||
handlers::{album_handlers, auth_handlers, media_handlers, person_handlers, tag_handlers, user_handlers},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -10,11 +10,17 @@ pub fn api_routes(max_upload_size: usize) -> Router<AppState> {
|
||||
let user_routes = user_handlers::user_routes();
|
||||
let media_routes = media_handlers::media_routes(max_upload_size);
|
||||
let album_routes = album_handlers::album_routes();
|
||||
let media_tag_routes = tag_handlers::tag_routes();
|
||||
let people_routes = person_handlers::people_routes();
|
||||
let face_routes = person_handlers::face_routes();
|
||||
|
||||
Router::new()
|
||||
.route("/api/v1/health", get(|| async { "OK" }))
|
||||
.nest("/api/v1/auth", auth_routes)
|
||||
.nest("/api/v1/users", user_routes)
|
||||
.nest("/api/v1/media", media_routes)
|
||||
.nest("/api/v1/media/{media_id}/tags", media_tag_routes)
|
||||
.nest("/api/v1/albums", album_routes)
|
||||
.nest("/api/v1/people", people_routes)
|
||||
.nest("/api/v1", face_routes)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use libertas_core::{models::{Album, AlbumPermission, MediaMetadata}};
|
||||
use libertas_core::models::{Album, AlbumPermission, FaceRegion, MediaMetadata, Person, PersonPermission, Tag};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -121,3 +121,78 @@ pub struct MediaDetailsResponse {
|
||||
pub metadata: Vec<MediaMetadataResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TagResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<Tag> for TagResponse {
|
||||
fn from(tag: Tag) -> Self {
|
||||
Self { id: tag.id, name: tag.name }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MediaTagsRequest {
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PersonResponse {
|
||||
pub id: Uuid,
|
||||
pub owner_id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<Person> for PersonResponse {
|
||||
fn from(person: Person) -> Self {
|
||||
Self { id: person.id, owner_id: person.owner_id, name: person.name }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreatePersonRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdatePersonRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct FaceRegionResponse {
|
||||
pub id: Uuid,
|
||||
pub media_id: Uuid,
|
||||
pub person_id: Option<Uuid>,
|
||||
pub x_min: f32,
|
||||
pub y_min: f32,
|
||||
pub x_max: f32,
|
||||
pub y_max: f32,
|
||||
}
|
||||
|
||||
impl From<FaceRegion> for FaceRegionResponse {
|
||||
fn from(face: FaceRegion) -> Self {
|
||||
Self {
|
||||
id: face.id,
|
||||
media_id: face.media_id,
|
||||
person_id: face.person_id,
|
||||
x_min: face.x_min,
|
||||
y_min: face.y_min,
|
||||
x_max: face.x_max,
|
||||
y_max: face.y_max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AssignFaceRequest {
|
||||
pub person_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SharePersonRequest {
|
||||
pub target_user_id: Uuid,
|
||||
pub permission: PersonPermission,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod album_service;
|
||||
pub mod media_service;
|
||||
pub mod user_service;
|
||||
pub mod tag_service;
|
||||
pub mod person_service;
|
||||
228
libertas_api/src/services/person_service.rs
Normal file
228
libertas_api/src/services/person_service.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use libertas_core::{authz, error::{CoreError, CoreResult}, models::{FaceRegion, Media, Person, PersonPermission}, repositories::{FaceRegionRepository, MediaRepository, PersonRepository, PersonShareRepository}, services::PersonService};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct PersonServiceImpl {
|
||||
person_repo: Arc<dyn PersonRepository>,
|
||||
face_repo: Arc<dyn FaceRegionRepository>,
|
||||
media_repo: Arc<dyn MediaRepository>,
|
||||
person_share_repo: Arc<dyn PersonShareRepository>,
|
||||
}
|
||||
|
||||
impl PersonServiceImpl {
|
||||
pub fn new(
|
||||
person_repo: Arc<dyn PersonRepository>,
|
||||
face_repo: Arc<dyn FaceRegionRepository>,
|
||||
media_repo: Arc<dyn MediaRepository>,
|
||||
person_share_repo: Arc<dyn PersonShareRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
person_repo,
|
||||
face_repo,
|
||||
media_repo,
|
||||
person_share_repo,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_and_authorize_person_owner(
|
||||
&self,
|
||||
person_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Person> {
|
||||
let person = self
|
||||
.person_repo
|
||||
.find_by_id(person_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Person".to_string(), person_id))?;
|
||||
|
||||
if person.owner_id != user_id {
|
||||
return Err(CoreError::Auth(
|
||||
"User must be the owner to perform this action".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(person)
|
||||
}
|
||||
|
||||
async fn get_and_authorize_person_access(
|
||||
&self,
|
||||
person_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Person> {
|
||||
let person = self
|
||||
.person_repo
|
||||
.find_by_id(person_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Person".to_string(), person_id))?;
|
||||
|
||||
let share_permission = self.person_share_repo.get_user_permission(person_id, user_id).await?;
|
||||
|
||||
if !authz::can_access_person(user_id, &person, share_permission) {
|
||||
return Err(CoreError::Auth(
|
||||
"User does not have permission to access this person".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(person)
|
||||
}
|
||||
|
||||
async fn get_and_authorize_person_usage(
|
||||
&self,
|
||||
person_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Person> {
|
||||
let person = self
|
||||
.person_repo
|
||||
.find_by_id(person_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Person".to_string(), person_id))?;
|
||||
|
||||
let share_permission = self.person_share_repo.get_user_permission(person_id, user_id).await?;
|
||||
|
||||
if !authz::can_edit_person(user_id, &person, share_permission) {
|
||||
return Err(CoreError::Auth(
|
||||
"User does not have permission to use this person".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(person)
|
||||
}
|
||||
|
||||
async fn authorize_media_access(
|
||||
&self,
|
||||
media_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Media> {
|
||||
let media = self.media_repo.find_by_id(media_id).await?.ok_or(CoreError::NotFound("Media".to_string(), media_id))?;
|
||||
|
||||
if !authz::is_owner(user_id, &media) {
|
||||
return Err(CoreError::Auth(
|
||||
"User does not have permission to access this media".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(media)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PersonService for PersonServiceImpl {
|
||||
async fn create_person(
|
||||
&self,
|
||||
name: &str,
|
||||
owner_id: Uuid,
|
||||
) -> CoreResult<Person> {
|
||||
let person = Person {
|
||||
id: Uuid::new_v4(),
|
||||
owner_id,
|
||||
name: name.to_string(),
|
||||
thumbnail_media_id: None,
|
||||
};
|
||||
|
||||
self.person_repo.create(person.clone()).await?;
|
||||
|
||||
Ok(person)
|
||||
}
|
||||
|
||||
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person> {
|
||||
self.get_and_authorize_person_access(person_id, user_id).await
|
||||
}
|
||||
|
||||
async fn list_people(&self, user_id: Uuid) -> CoreResult<Vec<Person>> {
|
||||
let mut owned_people = self.person_repo.list_by_user(user_id).await?;
|
||||
|
||||
let shared_people_with_perms = self
|
||||
.person_share_repo
|
||||
.list_people_shared_with_user(user_id)
|
||||
.await?;
|
||||
|
||||
let shared_people = shared_people_with_perms
|
||||
.into_iter()
|
||||
.map(|(person, _permission)| person)
|
||||
.collect::<Vec<Person>>();
|
||||
|
||||
owned_people.extend(shared_people);
|
||||
|
||||
Ok(owned_people)
|
||||
}
|
||||
|
||||
async fn update_person(
|
||||
&self,
|
||||
person_id: Uuid,
|
||||
name: &str,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Person> {
|
||||
let mut person = self.get_and_authorize_person_owner(person_id, user_id).await?;
|
||||
person.name = name.to_string();
|
||||
self.person_repo.update(person.clone()).await?;
|
||||
Ok(person)
|
||||
}
|
||||
|
||||
async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||
self.get_and_authorize_person_owner(person_id, user_id).await?;
|
||||
self.person_repo.delete(person_id).await
|
||||
}
|
||||
|
||||
async fn assign_face_to_person(
|
||||
&self,
|
||||
face_region_id: Uuid,
|
||||
person_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<FaceRegion> {
|
||||
self.get_and_authorize_person_usage(person_id, user_id).await?;
|
||||
|
||||
let mut face = self
|
||||
.face_repo
|
||||
.find_by_id(face_region_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("FaceRegion".to_string(), face_region_id))?;
|
||||
|
||||
self.authorize_media_access(face.media_id, user_id).await?;
|
||||
|
||||
self.face_repo
|
||||
.update_person_id(face_region_id, person_id)
|
||||
.await?;
|
||||
face.person_id = Some(person_id);
|
||||
|
||||
Ok(face)
|
||||
}
|
||||
|
||||
async fn list_faces_for_media(
|
||||
&self,
|
||||
media_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Vec<FaceRegion>> {
|
||||
self.authorize_media_access(media_id, user_id).await?;
|
||||
|
||||
self.face_repo.find_by_media_id(media_id).await
|
||||
}
|
||||
|
||||
async fn share_person(
|
||||
&self,
|
||||
person_id: Uuid,
|
||||
target_user_id: Uuid,
|
||||
permission: PersonPermission,
|
||||
owner_id: Uuid,
|
||||
) -> CoreResult<()> {
|
||||
self.get_and_authorize_person_owner(person_id, owner_id).await?;
|
||||
|
||||
self.person_share_repo
|
||||
.create_or_update_share(person_id, target_user_id, permission)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn unshare_person(
|
||||
&self,
|
||||
person_id: Uuid,
|
||||
target_user_id: Uuid,
|
||||
owner_id: Uuid,
|
||||
) -> CoreResult<()> {
|
||||
self.get_and_authorize_person_owner(person_id, owner_id).await?;
|
||||
|
||||
self.person_share_repo
|
||||
.remove_share(person_id, target_user_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
92
libertas_api/src/services/tag_service.rs
Normal file
92
libertas_api/src/services/tag_service.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use libertas_core::{authz, error::{CoreError, CoreResult}, models::{Media, Tag}, repositories::{MediaRepository, TagRepository}, services::TagService};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TagServiceImpl {
|
||||
tag_repo: Arc<dyn TagRepository>,
|
||||
media_repo: Arc<dyn MediaRepository>,
|
||||
}
|
||||
|
||||
impl TagServiceImpl {
|
||||
pub fn new(
|
||||
tag_repo: Arc<dyn TagRepository>,
|
||||
media_repo: Arc<dyn MediaRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tag_repo,
|
||||
media_repo,
|
||||
}
|
||||
}
|
||||
|
||||
async fn authorize_media_access(
|
||||
&self,
|
||||
media_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<Media> {
|
||||
let media = self.media_repo.find_by_id(media_id).await?.ok_or(CoreError::NotFound("Media".to_string(), media_id))?;
|
||||
|
||||
if !authz::is_owner(user_id, &media) {
|
||||
return Err(CoreError::Auth(
|
||||
"User does not have permission to access this media".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(media)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TagService for TagServiceImpl {
|
||||
async fn add_tags_to_media(
|
||||
&self,
|
||||
media_id: Uuid,
|
||||
tag_names: &[String],
|
||||
user_id: Uuid,
|
||||
|
||||
) -> CoreResult<Vec<Tag>> {
|
||||
self.authorize_media_access(media_id, user_id).await?;
|
||||
|
||||
let mut tag_ids = Vec::new();
|
||||
let tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
||||
for tag in &tags {
|
||||
tag_ids.push(tag.id);
|
||||
}
|
||||
|
||||
self.tag_repo.add_tags_to_media(media_id, &tag_ids).await?;
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
async fn remove_tags_from_media(
|
||||
&self,
|
||||
media_id: Uuid,
|
||||
tag_names: &[String],
|
||||
user_id: Uuid,
|
||||
) -> CoreResult<()> {
|
||||
self.authorize_media_access(media_id, user_id).await?;
|
||||
|
||||
let tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
||||
let mut tag_ids = Vec::new();
|
||||
for tag in &tags {
|
||||
tag_ids.push(tag.id);
|
||||
}
|
||||
|
||||
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<Vec<Tag>> {
|
||||
self.authorize_media_access(media_id, user_id).await?;
|
||||
|
||||
let tags = self.tag_repo.list_tags_for_media(media_id).await?;
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use libertas_core::{
|
||||
config::AppConfig,
|
||||
services::{AlbumService, MediaService, UserService},
|
||||
services::{AlbumService, MediaService, PersonService, TagService, UserService},
|
||||
};
|
||||
|
||||
use crate::security::TokenGenerator;
|
||||
@@ -12,6 +12,8 @@ pub struct AppState {
|
||||
pub user_service: Arc<dyn UserService>,
|
||||
pub media_service: Arc<dyn MediaService>,
|
||||
pub album_service: Arc<dyn AlbumService>,
|
||||
pub tag_service: Arc<dyn TagService>,
|
||||
pub person_service: Arc<dyn PersonService>,
|
||||
pub token_generator: Arc<dyn TokenGenerator>,
|
||||
pub nats_client: async_nats::Client,
|
||||
pub config: AppConfig,
|
||||
|
||||
Reference in New Issue
Block a user