feat: frontend MVP — auth, timeline, upload, albums, admin, image viewer
Backend: - user roles (DB + JWT + first-user-is-admin) - volume-aware file resolver (multi-volume asset serving) - directory scanner uses volume URI directly - date-summary endpoint (capture date from EXIF) - timeline ordered by capture date - list endpoints: volumes, plugins, pipelines, library paths - delete endpoints: volumes, library paths - configurable upload body limit (MAX_UPLOAD_BYTES) Frontend: - auth: login/register, token refresh, role-based admin gate - timeline: date-grouped grid, infinite scroll, date scrubber - image viewer: fullscreen zoom/pan/pinch, metadata sidebar - upload: drag-drop, sequential upload, progress tracking - albums: create, add/remove photos, asset picker dialog - admin: storage (import library), jobs (pagination, error details), plugins (list + toggle), pipelines, sidecars, duplicates - multi-select mode with add-to-album action - TanStack Query for all data fetching
This commit is contained in:
@@ -6,6 +6,7 @@ pub struct Config {
|
||||
pub nats_url: String,
|
||||
pub jwt_secret: String,
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
pub max_upload_bytes: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -26,6 +27,10 @@ impl Config {
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect(),
|
||||
max_upload_bytes: std::env::var("MAX_UPLOAD_BYTES")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(256 * 1024 * 1024),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use axum::Router;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::http::HeaderValue;
|
||||
use std::sync::Arc;
|
||||
use tower_http::{
|
||||
@@ -32,7 +33,8 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
||||
);
|
||||
|
||||
let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string());
|
||||
let file_storage: Arc<LocalFileStorage> = Arc::new(LocalFileStorage::new(&storage_path));
|
||||
let file_storage: Arc<dyn domain::ports::FileStoragePort> =
|
||||
Arc::new(LocalFileStorage::new(&storage_path));
|
||||
|
||||
// Build per-context services
|
||||
let identity = services::identity::build(&pool, &config.jwt_secret);
|
||||
@@ -68,6 +70,7 @@ pub async fn build_app(config: &Config) -> Result<Router> {
|
||||
|
||||
Ok(app_router(&state)
|
||||
.with_state(state)
|
||||
.layer(DefaultBodyLimit::max(config.max_upload_bytes))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(cors))
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ use adapters_postgres::{
|
||||
PostgresDerivativeRepository, PostgresDuplicateRepository, PostgresIngestTransaction,
|
||||
PostgresSidecarRepository,
|
||||
};
|
||||
use adapters_storage::LocalFileStorage;
|
||||
use adapters_storage::LocalVolumeFileResolver;
|
||||
use domain::ports::FileStoragePort;
|
||||
use application::catalog::{
|
||||
CreateStackHandler, DeleteAssetHandler, DeleteStackHandler, DetectLivePhotosHandler,
|
||||
GetAssetHandler, GetStackHandler, GetTimelineHandler, ListDuplicatesHandler,
|
||||
GetAssetHandler, GetDateSummaryHandler, GetStackHandler, GetTimelineHandler,
|
||||
ListDuplicatesHandler,
|
||||
ReadAssetFileHandler, ReadDerivativeHandler, RegisterAssetHandler, ResolveDuplicateHandler,
|
||||
SearchAssetsHandler, UpdateMetadataHandler,
|
||||
};
|
||||
@@ -21,7 +23,7 @@ use super::storage::StorageRepos;
|
||||
pub fn build(
|
||||
pool: &PgPool,
|
||||
storage_repos: &StorageRepos,
|
||||
file_storage: Arc<LocalFileStorage>,
|
||||
file_storage: Arc<dyn FileStoragePort>,
|
||||
event_publisher: Arc<dyn EventPublisher>,
|
||||
) -> CatalogHandlers {
|
||||
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
|
||||
@@ -49,15 +51,20 @@ pub fn build(
|
||||
metadata_repo.clone(),
|
||||
));
|
||||
|
||||
let get_date_summary = Arc::new(GetDateSummaryHandler::new(asset_repo.clone()));
|
||||
|
||||
let update_metadata = Arc::new(UpdateMetadataHandler::new(
|
||||
asset_repo.clone(),
|
||||
metadata_repo.clone(),
|
||||
event_publisher.clone(),
|
||||
));
|
||||
|
||||
let volume_resolver = Arc::new(LocalVolumeFileResolver::new(
|
||||
storage_repos.volume_repo.clone(),
|
||||
));
|
||||
let read_asset_file = Arc::new(ReadAssetFileHandler::new(
|
||||
asset_repo.clone(),
|
||||
file_storage.clone(),
|
||||
volume_resolver,
|
||||
));
|
||||
|
||||
let read_derivative = Arc::new(ReadDerivativeHandler::new(
|
||||
@@ -103,6 +110,7 @@ pub fn build(
|
||||
ingest_asset,
|
||||
get_asset,
|
||||
get_timeline,
|
||||
get_date_summary,
|
||||
update_metadata,
|
||||
read_asset_file,
|
||||
read_derivative,
|
||||
|
||||
@@ -26,9 +26,10 @@ pub fn build(pool: &PgPool, jwt_secret: &str) -> IdentityServices {
|
||||
issuer.clone(),
|
||||
refresh_repo.clone(),
|
||||
));
|
||||
let get_profile = Arc::new(GetProfileHandler::new(user_repo));
|
||||
let get_profile = Arc::new(GetProfileHandler::new(user_repo.clone()));
|
||||
let refresh = Arc::new(RefreshTokenHandler::new(
|
||||
refresh_repo.clone(),
|
||||
user_repo,
|
||||
issuer.clone(),
|
||||
));
|
||||
let logout = Arc::new(LogoutHandler::new(refresh_repo.clone()));
|
||||
|
||||
@@ -6,7 +6,8 @@ use adapters_postgres::{
|
||||
};
|
||||
use application::processing::{
|
||||
CompleteJobHandler, ConfigurePipelineHandler, EnqueueJobHandler, FailJobHandler,
|
||||
ListJobsHandler, ManagePluginHandler, ReportBatchProgressHandler, StartJobHandler,
|
||||
ListJobsHandler, ListPipelinesHandler, ListPluginsHandler, ManagePluginHandler,
|
||||
ReportBatchProgressHandler, StartJobHandler,
|
||||
};
|
||||
use domain::ports::EventPublisher;
|
||||
use presentation::state::ProcessingHandlers;
|
||||
@@ -35,6 +36,8 @@ pub fn build(pool: &PgPool, event_publisher: Arc<dyn EventPublisher>) -> Process
|
||||
let list_jobs = Arc::new(ListJobsHandler::new(job_repo.clone()));
|
||||
let batch_progress = Arc::new(ReportBatchProgressHandler::new(batch_repo, job_repo));
|
||||
let manage_plugin = Arc::new(ManagePluginHandler::new(plugin_repo.clone()));
|
||||
let list_plugins = Arc::new(ListPluginsHandler::new(plugin_repo.clone()));
|
||||
let list_pipelines = Arc::new(ListPipelinesHandler::new(pipeline_repo.clone()));
|
||||
let configure_pipeline = Arc::new(ConfigurePipelineHandler::new(pipeline_repo, plugin_repo));
|
||||
|
||||
ProcessingHandlers {
|
||||
@@ -45,6 +48,8 @@ pub fn build(pool: &PgPool, event_publisher: Arc<dyn EventPublisher>) -> Process
|
||||
list_jobs,
|
||||
batch_progress,
|
||||
manage_plugin,
|
||||
list_plugins,
|
||||
configure_pipeline,
|
||||
list_pipelines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ use adapters_postgres::{
|
||||
PgPool, PostgresLibraryPathRepository, PostgresQuotaRepository,
|
||||
PostgresStorageVolumeRepository, PostgresUsageLedgerRepository,
|
||||
};
|
||||
use application::storage::{CheckQuotaHandler, RegisterLibraryPathHandler, RegisterVolumeHandler};
|
||||
use application::storage::{
|
||||
CheckQuotaHandler, DeleteLibraryPathHandler, DeleteVolumeHandler, ListAllLibraryPathsHandler,
|
||||
ListIngestPathsHandler, ListVolumesHandler, RegisterLibraryPathHandler,
|
||||
RegisterVolumeHandler,
|
||||
};
|
||||
use presentation::state::StorageHandlers;
|
||||
|
||||
/// Shared storage repos needed by other bounded contexts (catalog ingest, etc.).
|
||||
pub struct StorageRepos {
|
||||
pub path_repo: Arc<PostgresLibraryPathRepository>,
|
||||
pub path_repo: Arc<dyn domain::ports::LibraryPathRepository>,
|
||||
pub volume_repo: Arc<dyn domain::ports::StorageVolumeRepository>,
|
||||
}
|
||||
|
||||
pub fn build(pool: &PgPool) -> (StorageRepos, StorageHandlers) {
|
||||
@@ -18,20 +23,33 @@ pub fn build(pool: &PgPool) -> (StorageRepos, StorageHandlers) {
|
||||
let quota_repo = Arc::new(PostgresQuotaRepository::new(pool.clone()));
|
||||
let ledger_repo = Arc::new(PostgresUsageLedgerRepository::new(pool.clone()));
|
||||
|
||||
let list_volumes = Arc::new(ListVolumesHandler::new(volume_repo.clone()));
|
||||
let register_volume = Arc::new(RegisterVolumeHandler::new(volume_repo.clone()));
|
||||
let delete_volume = Arc::new(DeleteVolumeHandler::new(volume_repo.clone()));
|
||||
let register_library_path = Arc::new(RegisterLibraryPathHandler::new(
|
||||
volume_repo,
|
||||
volume_repo.clone(),
|
||||
path_repo.clone(),
|
||||
));
|
||||
let list_ingest_paths = Arc::new(ListIngestPathsHandler::new(path_repo.clone()));
|
||||
let list_all_library_paths = Arc::new(ListAllLibraryPathsHandler::new(path_repo.clone()));
|
||||
let delete_library_path = Arc::new(DeleteLibraryPathHandler::new(path_repo.clone()));
|
||||
let check_quota = Arc::new(CheckQuotaHandler::new(quota_repo, ledger_repo));
|
||||
|
||||
let handlers = StorageHandlers {
|
||||
register_volume,
|
||||
delete_volume,
|
||||
list_volumes,
|
||||
register_library_path,
|
||||
list_ingest_paths,
|
||||
list_all_library_paths,
|
||||
delete_library_path,
|
||||
check_quota,
|
||||
};
|
||||
|
||||
let repos = StorageRepos { path_repo };
|
||||
let repos = StorageRepos {
|
||||
path_repo,
|
||||
volume_repo,
|
||||
};
|
||||
|
||||
(repos, handlers)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user