init
This commit is contained in:
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>,
|
||||
}
|
||||
Reference in New Issue
Block a user