init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
target/
|
||||||
|
.sqlx/
|
||||||
|
media_library/
|
||||||
2646
Cargo.lock
generated
Normal file
2646
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = ["libertas_api", "libertas_core"]
|
||||||
1
libertas_api/.env.example
Normal file
1
libertas_api/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL="postgres://postgres:postgres@localhost:5432/libertas_db"
|
||||||
4
libertas_api/.gitignore
vendored
Normal file
4
libertas_api/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
media_library/
|
||||||
|
.sqlx/
|
||||||
|
.env
|
||||||
32
libertas_api/Cargo.toml
Normal file
32
libertas_api/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "libertas_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.8.6", features = ["multipart"] }
|
||||||
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
|
|
||||||
|
libertas_core = { path = "../libertas_core" }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.8.6", features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"postgres",
|
||||||
|
"uuid",
|
||||||
|
"chrono",
|
||||||
|
"sqlite",
|
||||||
|
] }
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
chrono = { version = "0.4.42", features = ["serde"] }
|
||||||
|
uuid = { version = "1.18.1", features = ["v4", "serde"] }
|
||||||
|
anyhow = "1.0.100"
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
jsonwebtoken = { version = "10.1.0", features = ["rust_crypto"] }
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
headers = "0.4.1"
|
||||||
|
axum-extra = { version = "0.12.0", features = ["typed-header"] }
|
||||||
|
rand_core = { version = "0.9.3", features = ["std"] }
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
futures = "0.3.31"
|
||||||
|
bytes = "1.10.1"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Active: 1762068220033@@127.0.0.1@5432@libertas_db
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
hashed_password TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE media (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
owner_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
storage_path TEXT NOT NULL,
|
||||||
|
original_filename TEXT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL UNIQUE, -- For duplicate checking
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Optional fields from the model
|
||||||
|
extracted_location TEXT, width INTEGER, height INTEGER );
|
||||||
|
|
||||||
|
-- Indexes for faster lookups
|
||||||
|
CREATE INDEX idx_media_owner_id ON media (owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_media_hash ON media (hash);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Create the 'albums' table
|
||||||
|
CREATE TABLE albums (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
owner_id UUID NOT NULL REFERENCES users (id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create the 'album_media' join table
|
||||||
|
-- This links media items to albums
|
||||||
|
CREATE TABLE album_media (
|
||||||
|
album_id UUID NOT NULL REFERENCES albums (id) ON DELETE CASCADE,
|
||||||
|
media_id UUID NOT NULL REFERENCES media (id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (album_id, media_id) -- Ensures no duplicates
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for faster lookups
|
||||||
|
CREATE INDEX idx_albums_owner_id ON albums (owner_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_album_media_media_id ON album_media (media_id);
|
||||||
34
libertas_api/src/config.rs
Normal file
34
libertas_api/src/config.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use libertas_core::error::CoreResult;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum DatabaseType {
|
||||||
|
Postgres,
|
||||||
|
Sqlite,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct DatabaseConfig {
|
||||||
|
pub db_type: DatabaseType,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub database: DatabaseConfig,
|
||||||
|
pub server_address: String,
|
||||||
|
pub jwt_secret: String,
|
||||||
|
pub media_library_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> CoreResult<Config> {
|
||||||
|
Ok(Config {
|
||||||
|
database: DatabaseConfig {
|
||||||
|
db_type: DatabaseType::Postgres,
|
||||||
|
url: "postgres://postgres:postgres@localhost:5432/libertas_db".to_string(),
|
||||||
|
},
|
||||||
|
server_address: "127.0.0.1:8080".to_string(),
|
||||||
|
jwt_secret: "super_secret_jwt_key".to_string(),
|
||||||
|
media_library_path: "media_library".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
43
libertas_api/src/error.rs
Normal file
43
libertas_api/src/error.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use libertas_core::error::CoreError;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub struct ApiError(CoreError);
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let (status, error_message) = match self.0 {
|
||||||
|
CoreError::Database(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Database error: {}", e),
|
||||||
|
),
|
||||||
|
CoreError::Validation(e) => (StatusCode::BAD_REQUEST, e),
|
||||||
|
CoreError::NotFound(res, id) => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("{} with id {} not found", res, id),
|
||||||
|
),
|
||||||
|
CoreError::Duplicate(e) => (StatusCode::CONFLICT, e),
|
||||||
|
CoreError::Auth(e) => (StatusCode::UNAUTHORIZED, e),
|
||||||
|
_ => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"An unknown error occurred".to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = Json(json!({ "error": error_message }));
|
||||||
|
(status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for ApiError
|
||||||
|
where
|
||||||
|
E: Into<CoreError>,
|
||||||
|
{
|
||||||
|
fn from(err: E) -> Self {
|
||||||
|
Self(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
105
libertas_api/src/factory.rs
Normal file
105
libertas_api/src/factory.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use libertas_core::{
|
||||||
|
error::{CoreError, CoreResult},
|
||||||
|
repositories::UserRepository,
|
||||||
|
};
|
||||||
|
use sqlx::{Pool, Postgres, Sqlite};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, DatabaseConfig, DatabaseType},
|
||||||
|
repositories::user_repository::{PostgresUserRepository, SqliteUserRepository},
|
||||||
|
security::{Argon2Hasher, JwtGenerator},
|
||||||
|
services::{
|
||||||
|
album_service::AlbumServiceImpl, media_service::MediaServiceImpl,
|
||||||
|
user_service::UserServiceImpl,
|
||||||
|
},
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum DatabasePool {
|
||||||
|
Postgres(Pool<Postgres>),
|
||||||
|
Sqlite(Pool<Sqlite>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_app_state(config: Config) -> CoreResult<AppState> {
|
||||||
|
let db_pool = build_database_pool(&config.database).await?;
|
||||||
|
|
||||||
|
let user_repo = build_user_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
let media_repo = build_media_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
let album_repo = build_album_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
|
||||||
|
let hasher = Arc::new(Argon2Hasher::default());
|
||||||
|
let tokenizer = Arc::new(JwtGenerator::new(config.jwt_secret.clone()));
|
||||||
|
|
||||||
|
let user_service = Arc::new(UserServiceImpl::new(user_repo, hasher, tokenizer.clone()));
|
||||||
|
let media_service = Arc::new(MediaServiceImpl::new(media_repo.clone(), config.clone()));
|
||||||
|
let album_service = Arc::new(AlbumServiceImpl::new(album_repo, media_repo));
|
||||||
|
|
||||||
|
Ok(AppState {
|
||||||
|
user_service,
|
||||||
|
media_service,
|
||||||
|
album_service,
|
||||||
|
token_generator: tokenizer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_database_pool(db_config: &DatabaseConfig) -> CoreResult<DatabasePool> {
|
||||||
|
match db_config.db_type {
|
||||||
|
DatabaseType::Postgres => {
|
||||||
|
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||||
|
.max_connections(50)
|
||||||
|
.connect(&db_config.url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(DatabasePool::Postgres(pool))
|
||||||
|
}
|
||||||
|
DatabaseType::Sqlite => {
|
||||||
|
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
||||||
|
.max_connections(2)
|
||||||
|
.connect(&db_config.url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(DatabasePool::Sqlite(pool))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_user_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn UserRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(PostgresUserRepository::new(pg_pool))),
|
||||||
|
DatabasePool::Sqlite(sqlite_pool) => Ok(Arc::new(SqliteUserRepository::new(sqlite_pool))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_media_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn libertas_core::repositories::MediaRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||||
|
crate::repositories::media_repository::PostgresMediaRepository::new(pg_pool),
|
||||||
|
)),
|
||||||
|
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||||
|
"Sqlite media repository not implemented".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_album_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn libertas_core::repositories::AlbumRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||||
|
crate::repositories::album_repository::PostgresAlbumRepository::new(pg_pool),
|
||||||
|
)),
|
||||||
|
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||||
|
"Sqlite album repository not implemented".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
64
libertas_api/src/handlers/album_handlers.rs
Normal file
64
libertas_api/src/handlers/album_handlers.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use libertas_core::schema::{AddMediaToAlbumData, CreateAlbumData};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateAlbumRequest {
|
||||||
|
name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
is_public: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_album(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Json(payload): Json<CreateAlbumRequest>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
let album_data = CreateAlbumData {
|
||||||
|
owner_id: user_id,
|
||||||
|
name: &payload.name,
|
||||||
|
description: payload.description.as_deref(),
|
||||||
|
is_public: payload.is_public.unwrap_or(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.album_service.create_album(album_data).await?;
|
||||||
|
|
||||||
|
Ok(StatusCode::CREATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct AddMediaToAlbumRequest {
|
||||||
|
media_ids: Vec<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_media_to_album(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(album_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<AddMediaToAlbumRequest>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
let data = AddMediaToAlbumData {
|
||||||
|
album_id,
|
||||||
|
media_ids: payload.media_ids,
|
||||||
|
};
|
||||||
|
|
||||||
|
state
|
||||||
|
.album_service
|
||||||
|
.add_media_to_album(data, user_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn album_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", axum::routing::post(create_album))
|
||||||
|
.route("/{album_id}/media", axum::routing::post(add_media_to_album))
|
||||||
|
}
|
||||||
79
libertas_api/src/handlers/auth_handlers.rs
Normal file
79
libertas_api/src/handlers/auth_handlers.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use libertas_core::schema::{CreateUserData, LoginUserData};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct UserResponse {
|
||||||
|
id: Uuid,
|
||||||
|
username: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<RegisterRequest>,
|
||||||
|
) -> Result<(StatusCode, Json<UserResponse>), ApiError> {
|
||||||
|
let user_data = CreateUserData {
|
||||||
|
username: &payload.username,
|
||||||
|
email: &payload.email,
|
||||||
|
password: &payload.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = state.user_service.register(user_data).await?;
|
||||||
|
|
||||||
|
let response = UserResponse {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, Json(response)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
pub username_or_email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<LoginRequest>,
|
||||||
|
) -> Result<Json<LoginResponse>, ApiError> {
|
||||||
|
let login_data = LoginUserData {
|
||||||
|
username_or_email: &payload.username_or_email,
|
||||||
|
password: &payload.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token = state.user_service.login(login_data).await?;
|
||||||
|
Ok(Json(LoginResponse { token }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_me(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
) -> Result<Json<UserResponse>, ApiError> {
|
||||||
|
let user = state.user_service.get_user_details(user_id).await?;
|
||||||
|
|
||||||
|
let response = UserResponse {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
};
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
77
libertas_api/src/handlers/media_handlers.rs
Normal file
77
libertas_api/src/handlers/media_handlers.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
extract::{DefaultBodyLimit, Multipart, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Json,
|
||||||
|
routing::post,
|
||||||
|
};
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use libertas_core::{error::CoreError, models::Media, schema::UploadMediaData};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MediaResponse {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
storage_path: String,
|
||||||
|
original_filename: String,
|
||||||
|
mime_type: String,
|
||||||
|
hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Media> for MediaResponse {
|
||||||
|
fn from(media: Media) -> Self {
|
||||||
|
Self {
|
||||||
|
id: media.id,
|
||||||
|
storage_path: media.storage_path,
|
||||||
|
original_filename: media.original_filename,
|
||||||
|
mime_type: media.mime_type,
|
||||||
|
hash: media.hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn media_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", post(upload_media))
|
||||||
|
.layer(DefaultBodyLimit::max(250 * 1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_media(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
mut multipart: Multipart,
|
||||||
|
) -> Result<(StatusCode, Json<MediaResponse>), ApiError> {
|
||||||
|
let field = multipart
|
||||||
|
.next_field()
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Validation(format!("Multipart error: {}", e)))?
|
||||||
|
.ok_or(ApiError::from(CoreError::Validation(
|
||||||
|
"No file provided in 'file' field".to_string(),
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
let filename = field.file_name().unwrap_or("unknown_file").to_string();
|
||||||
|
let mime_type = field
|
||||||
|
.content_type()
|
||||||
|
.unwrap_or("application/octet-stream")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let stream = field.map_err(|e| io::Error::new(io::ErrorKind::Other, e));
|
||||||
|
|
||||||
|
let boxed_stream: Box<
|
||||||
|
dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send + Unpin,
|
||||||
|
> = Box::new(stream);
|
||||||
|
|
||||||
|
let upload_data = UploadMediaData {
|
||||||
|
owner_id: user_id,
|
||||||
|
filename,
|
||||||
|
mime_type,
|
||||||
|
stream: boxed_stream,
|
||||||
|
};
|
||||||
|
|
||||||
|
let media = state.media_service.upload_media(upload_data).await?;
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, Json(media.into())))
|
||||||
|
}
|
||||||
3
libertas_api/src/handlers/mod.rs
Normal file
3
libertas_api/src/handlers/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod album_handlers;
|
||||||
|
pub mod auth_handlers;
|
||||||
|
pub mod media_handlers;
|
||||||
53
libertas_api/src/main.rs
Normal file
53
libertas_api/src/main.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::handlers::{
|
||||||
|
album_handlers, auth_handlers,
|
||||||
|
media_handlers::{self},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod error;
|
||||||
|
pub mod factory;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod middleware;
|
||||||
|
pub mod repositories;
|
||||||
|
pub mod security;
|
||||||
|
pub mod services;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let config = config::load_config()?;
|
||||||
|
let addr: SocketAddr = config.server_address.parse()?;
|
||||||
|
|
||||||
|
let app_state = factory::build_app_state(config).await?;
|
||||||
|
|
||||||
|
let auth_routes = Router::new()
|
||||||
|
.route("/register", post(auth_handlers::register))
|
||||||
|
.route("/login", post(auth_handlers::login));
|
||||||
|
|
||||||
|
let user_routes = Router::new().route("/me", get(auth_handlers::get_me));
|
||||||
|
let media_routes = media_handlers::media_routes();
|
||||||
|
let album_routes = album_handlers::album_routes();
|
||||||
|
|
||||||
|
let app = 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/albums", album_routes)
|
||||||
|
.with_state(app_state);
|
||||||
|
|
||||||
|
println!("Starting server at http://{}", addr);
|
||||||
|
|
||||||
|
let listner = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
|
||||||
|
axum::serve(listner, app.into_make_service()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
49
libertas_api/src/middleware/auth.rs
Normal file
49
libertas_api/src/middleware/auth.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use axum::{
|
||||||
|
RequestPartsExt,
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::{StatusCode, request::Parts},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use axum_extra::TypedHeader;
|
||||||
|
use axum_extra::headers::{Authorization, authorization::Bearer};
|
||||||
|
use libertas_core::error::{CoreError, CoreResult};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{security::TokenGenerator, state::AppState};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct UserId(pub Uuid);
|
||||||
|
|
||||||
|
impl FromRequestParts<AppState> for UserId {
|
||||||
|
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(user_id)),
|
||||||
|
Err(e) => {
|
||||||
|
let status = match e {
|
||||||
|
CoreError::Auth(_) => StatusCode::UNAUTHORIZED,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
};
|
||||||
|
Err((status, e.to_string()).into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libertas_api/src/middleware/mod.rs
Normal file
1
libertas_api/src/middleware/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod auth;
|
||||||
91
libertas_api/src/repositories/album_repository.rs
Normal file
91
libertas_api/src/repositories/album_repository.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{
|
||||||
|
error::{CoreError, CoreResult},
|
||||||
|
models::Album,
|
||||||
|
repositories::AlbumRepository,
|
||||||
|
};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresAlbumRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresAlbumRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AlbumRepository for PostgresAlbumRepository {
|
||||||
|
async fn create(&self, album: Album) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO albums (id, owner_id, name, description, is_public, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
"#,
|
||||||
|
album.id,
|
||||||
|
album.owner_id,
|
||||||
|
album.name,
|
||||||
|
album.description,
|
||||||
|
album.is_public,
|
||||||
|
album.created_at,
|
||||||
|
album.updated_at
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Album>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Album,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, name, description, is_public, created_at, updated_at
|
||||||
|
FROM albums
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_by_user(&self, user_id: Uuid) -> CoreResult<Vec<Album>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Album,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, name, description, is_public, created_at, updated_at
|
||||||
|
FROM albums
|
||||||
|
WHERE owner_id = $1
|
||||||
|
"#,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_media_to_album(&self, album_id: Uuid, media_ids: &[Uuid]) -> CoreResult<()> {
|
||||||
|
// Use sqlx's `unnest` feature to pass the Vec<Uuid> efficiently
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO album_media (album_id, media_id)
|
||||||
|
SELECT $1, media_id FROM unnest($2::uuid[]) as media_id
|
||||||
|
ON CONFLICT (album_id, media_id) DO NOTHING
|
||||||
|
"#,
|
||||||
|
album_id,
|
||||||
|
media_ids
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
93
libertas_api/src/repositories/media_repository.rs
Normal file
93
libertas_api/src/repositories/media_repository.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{
|
||||||
|
error::{CoreError, CoreResult},
|
||||||
|
models::Media,
|
||||||
|
repositories::MediaRepository,
|
||||||
|
};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresMediaRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresMediaRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MediaRepository for PostgresMediaRepository {
|
||||||
|
async fn create(&self, media: &Media) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO media (id, owner_id, storage_path, original_filename, mime_type, hash, created_at, width, height)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
"#,
|
||||||
|
media.id,
|
||||||
|
media.owner_id,
|
||||||
|
media.storage_path,
|
||||||
|
media.original_filename,
|
||||||
|
media.mime_type,
|
||||||
|
media.hash,
|
||||||
|
media.created_at,
|
||||||
|
media.width,
|
||||||
|
media.height
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_hash(&self, hash: &str) -> CoreResult<Option<Media>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Media,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at,
|
||||||
|
extracted_location, width, height
|
||||||
|
FROM media
|
||||||
|
WHERE hash = $1
|
||||||
|
"#,
|
||||||
|
hash
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Media>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Media,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at,
|
||||||
|
extracted_location, width, height
|
||||||
|
FROM media
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_by_user(&self, user_id: Uuid) -> CoreResult<Vec<Media>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Media,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at,
|
||||||
|
extracted_location, width, height
|
||||||
|
FROM media
|
||||||
|
WHERE owner_id = $1
|
||||||
|
"#,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
3
libertas_api/src/repositories/mod.rs
Normal file
3
libertas_api/src/repositories/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod album_repository;
|
||||||
|
pub mod media_repository;
|
||||||
|
pub mod user_repository;
|
||||||
96
libertas_api/src/repositories/user_repository.rs
Normal file
96
libertas_api/src/repositories/user_repository.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{
|
||||||
|
error::{CoreError, CoreResult},
|
||||||
|
models::User,
|
||||||
|
repositories::UserRepository,
|
||||||
|
};
|
||||||
|
use sqlx::{PgPool, SqlitePool, types::Uuid};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresUserRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresUserRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SqliteUserRepository {
|
||||||
|
_pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteUserRepository {
|
||||||
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
|
Self { _pool: pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserRepository for PostgresUserRepository {
|
||||||
|
async fn create(&self, user: User) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO users (id, username, email, hashed_password, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
"#,
|
||||||
|
user.id,
|
||||||
|
user.username,
|
||||||
|
user.email,
|
||||||
|
user.hashed_password,
|
||||||
|
user.created_at,
|
||||||
|
user.updated_at
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_email(&self, email: &str) -> CoreResult<Option<User>> {
|
||||||
|
sqlx::query_as!(User, "SELECT * FROM users WHERE email = $1", email)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_username(&self, username: &str) -> CoreResult<Option<User>> {
|
||||||
|
sqlx::query_as!(User, "SELECT * FROM users WHERE username = $1", username)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<User>> {
|
||||||
|
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl UserRepository for SqliteUserRepository {
|
||||||
|
async fn create(&self, _user: User) -> CoreResult<()> {
|
||||||
|
println!("SQLITE REPO: Creating user");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_email(&self, _email: &str) -> CoreResult<Option<User>> {
|
||||||
|
println!("SQLITE REPO: Finding user by email");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_username(&self, _username: &str) -> CoreResult<Option<User>> {
|
||||||
|
println!("SQLITE REPO: Finding user by username");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, _id: Uuid) -> CoreResult<Option<User>> {
|
||||||
|
println!("SQLITE REPO: Finding user by id");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
libertas_api/src/security.rs
Normal file
115
libertas_api/src/security.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use argon2::{
|
||||||
|
Argon2,
|
||||||
|
password_hash::{
|
||||||
|
PasswordHash, PasswordHasher as _, PasswordVerifier, SaltString, rand_core::OsRng,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||||
|
use libertas_core::error::{CoreError, CoreResult};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PasswordHasher: Send + Sync {
|
||||||
|
async fn hash_password(&self, password: &str) -> CoreResult<String>;
|
||||||
|
async fn verify_password(&self, password: &str, hash: &str) -> CoreResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Argon2Hasher;
|
||||||
|
|
||||||
|
impl Default for Argon2Hasher {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PasswordHasher for Argon2Hasher {
|
||||||
|
async fn hash_password(&self, password: &str) -> CoreResult<String> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
|
let password_bytes = password.as_bytes().to_vec();
|
||||||
|
|
||||||
|
let hash_string = tokio::task::spawn_blocking(move || {
|
||||||
|
Argon2::default()
|
||||||
|
.hash_password(&password_bytes, &salt)
|
||||||
|
.map(|hash| hash.to_string())
|
||||||
|
.map_err(|e| CoreError::Auth(format!("Password hashing failed: {}", e)))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()?;
|
||||||
|
|
||||||
|
Ok(hash_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_password(&self, password: &str, hash_str: &str) -> CoreResult<()> {
|
||||||
|
let hash = PasswordHash::new(hash_str)
|
||||||
|
.map_err(|e| CoreError::Auth(format!("Invalid password hash format: {}", e)))?;
|
||||||
|
|
||||||
|
let (password_bytes, hash_bytes) = (password.as_bytes().to_vec(), hash.to_string());
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
Argon2::default()
|
||||||
|
.verify_password(&password_bytes, &PasswordHash::new(&hash_bytes).unwrap())
|
||||||
|
.map_err(|_| CoreError::Auth("Invalid password".to_string()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static JWT_HEADER: Lazy<Header> = Lazy::new(Header::default);
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
struct Claims {
|
||||||
|
sub: Uuid,
|
||||||
|
exp: usize,
|
||||||
|
iat: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TokenGenerator: Send + Sync {
|
||||||
|
fn generate_token(&self, user_id: Uuid) -> CoreResult<String>;
|
||||||
|
fn verify_token(&self, token: &str) -> CoreResult<Uuid>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JwtGenerator {
|
||||||
|
encoding_key: EncodingKey,
|
||||||
|
decoding_key: DecodingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JwtGenerator {
|
||||||
|
pub fn new(secret: String) -> Self {
|
||||||
|
Self {
|
||||||
|
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
|
||||||
|
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TokenGenerator for JwtGenerator {
|
||||||
|
fn generate_token(&self, user_id: Uuid) -> CoreResult<String> {
|
||||||
|
let now = Utc::now();
|
||||||
|
let iat = now.timestamp() as usize;
|
||||||
|
let exp = (now + Duration::days(7)).timestamp() as usize;
|
||||||
|
|
||||||
|
let claims = Claims {
|
||||||
|
sub: user_id,
|
||||||
|
iat,
|
||||||
|
exp,
|
||||||
|
};
|
||||||
|
|
||||||
|
encode(&JWT_HEADER, &claims, &self.encoding_key)
|
||||||
|
.map_err(|e| CoreError::Auth(format!("Token generation failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_token(&self, token: &str) -> CoreResult<Uuid> {
|
||||||
|
decode::<Claims>(token, &self.decoding_key, &Validation::default())
|
||||||
|
.map(|data| data.claims.sub)
|
||||||
|
.map_err(|e| CoreError::Auth(format!("Token invalid: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
13
libertas_api/src/state.rs
Normal file
13
libertas_api/src/state.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use libertas_core::services::{AlbumService, MediaService, UserService};
|
||||||
|
|
||||||
|
use crate::security::TokenGenerator;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub user_service: Arc<dyn UserService>,
|
||||||
|
pub media_service: Arc<dyn MediaService>,
|
||||||
|
pub album_service: Arc<dyn AlbumService>,
|
||||||
|
pub token_generator: Arc<dyn TokenGenerator>,
|
||||||
|
}
|
||||||
1
libertas_core/.gitignore
vendored
Normal file
1
libertas_core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
13
libertas_core/Cargo.toml
Normal file
13
libertas_core/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "libertas_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.100"
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
bytes = "1.10.1"
|
||||||
|
chrono = "0.4.42"
|
||||||
|
futures = "0.3.31"
|
||||||
|
thiserror = "2.0.17"
|
||||||
|
uuid = "1.18.1"
|
||||||
30
libertas_core/src/error.rs
Normal file
30
libertas_core/src/error.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CoreError {
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
Config(String),
|
||||||
|
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(String),
|
||||||
|
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
|
||||||
|
#[error("{0} not found with id: {1}")]
|
||||||
|
NotFound(String, uuid::Uuid),
|
||||||
|
|
||||||
|
#[error("Duplicate resource: {0}")]
|
||||||
|
Duplicate(String),
|
||||||
|
|
||||||
|
#[error("Authentication failed: {0}")]
|
||||||
|
Auth(String),
|
||||||
|
|
||||||
|
#[error("An unknown error occurred")]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type CoreResult<T> = Result<T, CoreError>;
|
||||||
5
libertas_core/src/lib.rs
Normal file
5
libertas_core/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod models;
|
||||||
|
pub mod repositories;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod services;
|
||||||
67
libertas_core/src/models.rs
Normal file
67
libertas_core/src/models.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
pub struct Media {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub owner_id: uuid::Uuid,
|
||||||
|
pub storage_path: String,
|
||||||
|
pub original_filename: String,
|
||||||
|
pub mime_type: String,
|
||||||
|
pub hash: String,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub extracted_location: Option<String>,
|
||||||
|
pub width: Option<i32>,
|
||||||
|
pub height: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub email: String,
|
||||||
|
pub hashed_password: String,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Album {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub owner_id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub is_public: bool,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Person {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub owner_id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub thumbnail_media_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FaceRegion {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub media_id: uuid::Uuid,
|
||||||
|
pub person_id: Option<uuid::Uuid>,
|
||||||
|
|
||||||
|
pub x_min: f32,
|
||||||
|
pub y_min: f32,
|
||||||
|
pub x_max: f32,
|
||||||
|
pub y_max: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AlbumMedia {
|
||||||
|
pub album_id: uuid::Uuid,
|
||||||
|
pub media_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum AlbumPermission {
|
||||||
|
View,
|
||||||
|
Contribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AlbumShare {
|
||||||
|
pub album_id: uuid::Uuid,
|
||||||
|
pub user_id: uuid::Uuid,
|
||||||
|
pub permission: AlbumPermission,
|
||||||
|
}
|
||||||
31
libertas_core/src/repositories.rs
Normal file
31
libertas_core/src/repositories.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::CoreResult,
|
||||||
|
models::{Album, Media, User},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait MediaRepository: Send + Sync {
|
||||||
|
async fn find_by_hash(&self, hash: &str) -> CoreResult<Option<Media>>;
|
||||||
|
async fn create(&self, media: &Media) -> CoreResult<()>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Media>>;
|
||||||
|
async fn list_by_user(&self, user_id: Uuid) -> CoreResult<Vec<Media>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait UserRepository: Send + Sync {
|
||||||
|
async fn create(&self, user: User) -> CoreResult<()>;
|
||||||
|
async fn find_by_email(&self, email: &str) -> CoreResult<Option<User>>;
|
||||||
|
async fn find_by_username(&self, username: &str) -> CoreResult<Option<User>>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<User>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AlbumRepository: Send + Sync {
|
||||||
|
async fn create(&self, album: Album) -> CoreResult<()>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Album>>;
|
||||||
|
async fn list_by_user(&self, user_id: Uuid) -> CoreResult<Vec<Album>>;
|
||||||
|
async fn add_media_to_album(&self, album_id: Uuid, media_ids: &[Uuid]) -> CoreResult<()>;
|
||||||
|
}
|
||||||
38
libertas_core/src/schema.rs
Normal file
38
libertas_core/src/schema.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::models::AlbumPermission;
|
||||||
|
|
||||||
|
pub struct UploadMediaData<'a> {
|
||||||
|
pub owner_id: uuid::Uuid,
|
||||||
|
pub filename: String,
|
||||||
|
pub mime_type: String,
|
||||||
|
pub stream:
|
||||||
|
Box<dyn futures::Stream<Item = Result<bytes::Bytes, std::io::Error>> + Send + Unpin + 'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateUserData<'a> {
|
||||||
|
pub username: &'a str,
|
||||||
|
pub email: &'a str,
|
||||||
|
pub password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoginUserData<'a> {
|
||||||
|
pub username_or_email: &'a str,
|
||||||
|
pub password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateAlbumData<'a> {
|
||||||
|
pub owner_id: uuid::Uuid,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub description: Option<&'a str>,
|
||||||
|
pub is_public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddMediaToAlbumData {
|
||||||
|
pub album_id: uuid::Uuid,
|
||||||
|
pub media_ids: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ShareAlbumData {
|
||||||
|
pub album_id: uuid::Uuid,
|
||||||
|
pub target_user_id: uuid::Uuid,
|
||||||
|
pub permission: AlbumPermission,
|
||||||
|
}
|
||||||
35
libertas_core/src/services.rs
Normal file
35
libertas_core/src/services.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::CoreResult,
|
||||||
|
models::{Album, Media, User},
|
||||||
|
schema::{
|
||||||
|
AddMediaToAlbumData, CreateAlbumData, CreateUserData, LoginUserData, ShareAlbumData,
|
||||||
|
UploadMediaData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait MediaService: Send + Sync {
|
||||||
|
async fn upload_media(&self, data: UploadMediaData<'_>) -> CoreResult<Media>;
|
||||||
|
async fn get_media_details(&self, id: Uuid, user_id: Uuid) -> CoreResult<Media>;
|
||||||
|
async fn list_user_media(&self, user_id: Uuid) -> CoreResult<Vec<Media>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait UserService: Send + Sync {
|
||||||
|
async fn register(&self, data: CreateUserData<'_>) -> CoreResult<User>;
|
||||||
|
async fn login(&self, data: LoginUserData<'_>) -> CoreResult<String>;
|
||||||
|
async fn get_user_details(&self, user_id: uuid::Uuid) -> CoreResult<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AlbumService: Send + Sync {
|
||||||
|
async fn create_album(&self, data: CreateAlbumData<'_>) -> CoreResult<()>;
|
||||||
|
async fn get_album_details(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<Album>;
|
||||||
|
async fn add_media_to_album(&self, data: AddMediaToAlbumData, user_id: Uuid) -> CoreResult<()>;
|
||||||
|
async fn list_user_albums(&self, user_id: Uuid) -> CoreResult<Vec<Album>>;
|
||||||
|
|
||||||
|
async fn share_album(&self, data: ShareAlbumData, owner_id: Uuid) -> CoreResult<()>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user