init
This commit is contained in:
113
libertas_api/src/services/album_service.rs
Normal file
113
libertas_api/src/services/album_service.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
models::Album,
|
||||
repositories::{AlbumRepository, MediaRepository},
|
||||
schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData},
|
||||
services::AlbumService,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct AlbumServiceImpl {
|
||||
album_repo: Arc<dyn AlbumRepository>,
|
||||
media_repo: Arc<dyn MediaRepository>,
|
||||
}
|
||||
|
||||
impl AlbumServiceImpl {
|
||||
pub fn new(album_repo: Arc<dyn AlbumRepository>, media_repo: Arc<dyn MediaRepository>) -> Self {
|
||||
Self {
|
||||
album_repo,
|
||||
media_repo,
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_album_owner(&self, user_id: Uuid, album_id: Uuid) -> CoreResult<bool> {
|
||||
let album = self
|
||||
.album_repo
|
||||
.find_by_id(album_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Album".to_string(), album_id))?;
|
||||
|
||||
Ok(album.owner_id == user_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AlbumService for AlbumServiceImpl {
|
||||
async fn create_album(&self, data: CreateAlbumData<'_>) -> CoreResult<()> {
|
||||
if data.name.is_empty() {
|
||||
return Err(CoreError::Validation(
|
||||
"Album name cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let album = Album {
|
||||
id: Uuid::new_v4(),
|
||||
owner_id: data.owner_id,
|
||||
name: data.name.to_string(),
|
||||
description: data.description.map(String::from),
|
||||
is_public: data.is_public,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
self.album_repo.create(album).await
|
||||
}
|
||||
|
||||
async fn get_album_details(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<Album> {
|
||||
let album = self
|
||||
.album_repo
|
||||
.find_by_id(album_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Album".to_string(), album_id))?;
|
||||
|
||||
// Security check: Only owner (for now) can see album details
|
||||
if album.owner_id != user_id {
|
||||
// Later, this would also check share permissions
|
||||
return Err(CoreError::Auth("Access denied to album".to_string()));
|
||||
}
|
||||
|
||||
Ok(album)
|
||||
}
|
||||
|
||||
async fn add_media_to_album(&self, data: AddMediaToAlbumData, user_id: Uuid) -> CoreResult<()> {
|
||||
// 1. Verify the user owns the album
|
||||
if !self.is_album_owner(user_id, data.album_id).await? {
|
||||
return Err(CoreError::Auth("User does not own this album".to_string()));
|
||||
}
|
||||
|
||||
// 2. Bonus: Verify the user owns all media items
|
||||
for media_id in &data.media_ids {
|
||||
let media = self
|
||||
.media_repo
|
||||
.find_by_id(*media_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Media".to_string(), *media_id))?;
|
||||
|
||||
if media.owner_id != user_id {
|
||||
return Err(CoreError::Auth(format!(
|
||||
"Access denied to media item {}",
|
||||
media_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Call the repository to add them
|
||||
self.album_repo
|
||||
.add_media_to_album(data.album_id, &data.media_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_user_albums(&self, user_id: Uuid) -> CoreResult<Vec<Album>> {
|
||||
self.album_repo.list_by_user(user_id).await
|
||||
}
|
||||
|
||||
async fn share_album(&self, _data: ShareAlbumData, _owner_id: Uuid) -> CoreResult<()> {
|
||||
// This is not part of the MVP, but part of the trait
|
||||
unimplemented!("Sharing will be implemented in a future phase")
|
||||
}
|
||||
}
|
||||
112
libertas_api/src/services/media_service.rs
Normal file
112
libertas_api/src/services/media_service.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Datelike;
|
||||
use futures::stream::StreamExt;
|
||||
use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
models::Media,
|
||||
repositories::MediaRepository,
|
||||
schema::UploadMediaData,
|
||||
services::MediaService,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub struct MediaServiceImpl {
|
||||
repo: Arc<dyn MediaRepository>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl MediaServiceImpl {
|
||||
pub fn new(repo: Arc<dyn MediaRepository>, config: Config) -> Self {
|
||||
Self { repo, config }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MediaService for MediaServiceImpl {
|
||||
async fn upload_media(&self, mut data: UploadMediaData<'_>) -> CoreResult<Media> {
|
||||
let mut hasher = Sha256::new();
|
||||
let mut file_bytes = Vec::new();
|
||||
|
||||
while let Some(chunk_result) = data.stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| CoreError::Io(e))?;
|
||||
hasher.update(&chunk);
|
||||
file_bytes.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
let hash = format!("{:x}", hasher.finalize());
|
||||
|
||||
if self.repo.find_by_hash(&hash).await?.is_some() {
|
||||
return Err(CoreError::Duplicate(
|
||||
"A file with this content already exists".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let year = now.year().to_string();
|
||||
let month = format!("{:02}", now.month());
|
||||
let mut dest_path = PathBuf::from(&self.config.media_library_path);
|
||||
dest_path.push(year.clone());
|
||||
dest_path.push(month.clone());
|
||||
|
||||
fs::create_dir_all(&dest_path)
|
||||
.await
|
||||
.map_err(|e| CoreError::Io(e))?;
|
||||
|
||||
dest_path.push(&data.filename);
|
||||
|
||||
let storage_path_str = PathBuf::from(&year)
|
||||
.join(&month)
|
||||
.join(&data.filename)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let mut file = fs::File::create(&dest_path)
|
||||
.await
|
||||
.map_err(|e| CoreError::Io(e))?;
|
||||
|
||||
file.write_all(&file_bytes)
|
||||
.await
|
||||
.map_err(|e| CoreError::Io(e))?;
|
||||
|
||||
let media_model = Media {
|
||||
id: Uuid::new_v4(),
|
||||
owner_id: data.owner_id,
|
||||
storage_path: storage_path_str,
|
||||
original_filename: data.filename,
|
||||
mime_type: data.mime_type,
|
||||
hash,
|
||||
created_at: now,
|
||||
extracted_location: None,
|
||||
width: None,
|
||||
height: None,
|
||||
};
|
||||
|
||||
self.repo.create(&media_model).await?;
|
||||
|
||||
Ok(media_model)
|
||||
}
|
||||
|
||||
async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult<Media> {
|
||||
let media = self
|
||||
.repo
|
||||
.find_by_id(id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("Media".to_string(), id))?;
|
||||
|
||||
if media.owner_id != user_id {
|
||||
return Err(CoreError::Auth("Access denied".to_string()));
|
||||
}
|
||||
|
||||
Ok(media)
|
||||
}
|
||||
|
||||
async fn list_user_media(&self, user_id: Uuid) -> CoreResult<Vec<Media>> {
|
||||
self.repo.list_by_user(user_id).await
|
||||
}
|
||||
}
|
||||
3
libertas_api/src/services/mod.rs
Normal file
3
libertas_api/src/services/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod album_service;
|
||||
pub mod media_service;
|
||||
pub mod user_service;
|
||||
89
libertas_api/src/services/user_service.rs
Normal file
89
libertas_api/src/services/user_service.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use libertas_core::{
|
||||
error::{CoreError, CoreResult},
|
||||
models::User,
|
||||
repositories::UserRepository,
|
||||
schema::{CreateUserData, LoginUserData},
|
||||
services::UserService,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::security::{PasswordHasher, TokenGenerator};
|
||||
|
||||
pub struct UserServiceImpl {
|
||||
repo: Arc<dyn UserRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
tokenizer: Arc<dyn TokenGenerator>,
|
||||
}
|
||||
|
||||
impl UserServiceImpl {
|
||||
pub fn new(
|
||||
repo: Arc<dyn UserRepository>,
|
||||
hasher: Arc<dyn PasswordHasher>,
|
||||
tokenizer: Arc<dyn TokenGenerator>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
hasher,
|
||||
tokenizer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserService for UserServiceImpl {
|
||||
async fn register(&self, data: CreateUserData<'_>) -> CoreResult<User> {
|
||||
if data.username.is_empty() || data.email.is_empty() || data.password.is_empty() {
|
||||
return Err(CoreError::Validation(
|
||||
"Username, email, and password cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.repo.find_by_email(data.email).await?.is_some() {
|
||||
return Err(CoreError::Duplicate("Email already exists".to_string()));
|
||||
}
|
||||
if self.repo.find_by_username(data.username).await?.is_some() {
|
||||
return Err(CoreError::Duplicate("Username already exists".to_string()));
|
||||
}
|
||||
|
||||
let hashed_password = self.hasher.hash_password(data.password).await?;
|
||||
|
||||
let user = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: data.username.to_string(),
|
||||
email: data.email.to_string(),
|
||||
hashed_password,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
self.repo.create(user.clone()).await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn login(&self, data: LoginUserData<'_>) -> CoreResult<String> {
|
||||
let user = self
|
||||
.repo
|
||||
.find_by_email(data.username_or_email) // Allow login with email
|
||||
.await?
|
||||
.or(self.repo.find_by_username(data.username_or_email).await?) // Or username
|
||||
.ok_or(CoreError::Auth("Invalid credentials".to_string()))?;
|
||||
|
||||
self.hasher
|
||||
.verify_password(data.password, &user.hashed_password)
|
||||
.await?;
|
||||
|
||||
// 3. Generate JWT token
|
||||
self.tokenizer.generate_token(user.id)
|
||||
}
|
||||
|
||||
async fn get_user_details(&self, user_id: Uuid) -> CoreResult<User> {
|
||||
self.repo
|
||||
.find_by_id(user_id)
|
||||
.await?
|
||||
.ok_or(CoreError::NotFound("User".to_string(), user_id))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user