feat: auth hardening + codebase quality sweep
Refresh tokens: RefreshToken entity, PostgresRefreshTokenRepository, login returns refresh token, POST /auth/refresh (rotation), POST /auth/logout, JWT expiry 24h→1h, configurable via with_expiry(). Route protection: require_auth middleware on protected routes, public routes split (register, login, refresh, sharing/access). Authorization: caller_id added to ReadAssetFileQuery, ReadDerivativeQuery, GetStackQuery, DeleteStackCommand with ownership checks. Admin-only gates on processing, storage, sidecar, duplicates handlers. Quality fixes: visibility filtering bypass in search(), unwrap panics in date parsing, DRY auth header parsing, centralized parsers module, email validation via email_address crate, value objects (Username, MimeType, RelativePath), domain events (UserCreated, UserDeleted, AlbumCreated, TagCreated, DuplicateDetected), postgres error mapping for constraint violations, OptionExt::or_not_found helper, in_memory_repo! macro, GetStackQuery moved to queries, album add_entry 200→201.
This commit is contained in:
@@ -1,19 +1,30 @@
|
||||
use crate::{
|
||||
handlers::{albums, assets, auth, health, processing, sharing, sidecar, storage},
|
||||
handlers::{
|
||||
albums, assets, auth, duplicates, health, processing, sharing, sidecar, stacks, storage,
|
||||
},
|
||||
middleware::auth::require_auth,
|
||||
openapi::openapi_router,
|
||||
state::AppState,
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
middleware::from_fn_with_state,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
|
||||
pub fn api_v1_router() -> Router<AppState> {
|
||||
fn public_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
// auth
|
||||
.route("/auth/register", post(auth::register))
|
||||
.route("/auth/login", post(auth::login))
|
||||
.route("/auth/refresh", post(auth::refresh))
|
||||
.route("/sharing/access/{token}", get(sharing::access_by_token))
|
||||
}
|
||||
|
||||
fn protected_routes(state: &AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
// auth
|
||||
.route("/auth/me", get(auth::me))
|
||||
.route("/auth/logout", post(auth::logout))
|
||||
// albums
|
||||
.route("/albums", post(albums::create_album))
|
||||
.route("/albums/{id}", get(albums::get_album))
|
||||
@@ -23,10 +34,14 @@ pub fn api_v1_router() -> Router<AppState> {
|
||||
delete(albums::remove_entry),
|
||||
)
|
||||
// assets
|
||||
.route("/assets", get(assets::search_assets))
|
||||
.route("/assets/ingest", post(assets::ingest))
|
||||
.route("/assets/register", post(assets::register_asset))
|
||||
.route("/assets/timeline", get(assets::timeline))
|
||||
.route("/assets/{id}", get(assets::get_asset))
|
||||
.route(
|
||||
"/assets/{id}",
|
||||
get(assets::get_asset).delete(assets::delete_asset),
|
||||
)
|
||||
.route("/assets/{id}/metadata", put(assets::update_metadata))
|
||||
.route("/assets/{id}/file", get(assets::serve_file))
|
||||
.route(
|
||||
@@ -34,11 +49,26 @@ pub fn api_v1_router() -> Router<AppState> {
|
||||
get(assets::serve_derivative),
|
||||
)
|
||||
.route("/assets/{id}/tags", post(assets::tag_asset))
|
||||
// stacks
|
||||
.route("/stacks", post(stacks::create_stack))
|
||||
.route(
|
||||
"/stacks/detect-live-photos",
|
||||
post(stacks::detect_live_photos),
|
||||
)
|
||||
.route(
|
||||
"/stacks/{id}",
|
||||
get(stacks::get_stack).delete(stacks::delete_stack),
|
||||
)
|
||||
// duplicates
|
||||
.route("/duplicates", get(duplicates::list_duplicates))
|
||||
.route(
|
||||
"/duplicates/{id}/resolve",
|
||||
post(duplicates::resolve_duplicate),
|
||||
)
|
||||
// sharing
|
||||
.route("/sharing", post(sharing::share_resource))
|
||||
.route("/sharing/links", post(sharing::generate_link))
|
||||
.route("/sharing/{id}", delete(sharing::revoke))
|
||||
.route("/sharing/access/{token}", get(sharing::access_by_token))
|
||||
// storage
|
||||
.route("/storage/volumes", post(storage::register_volume))
|
||||
.route(
|
||||
@@ -57,18 +87,22 @@ pub fn api_v1_router() -> Router<AppState> {
|
||||
.route("/sidecar/full-export", post(sidecar::full_export))
|
||||
.route("/sidecar/full-import", post(sidecar::full_import))
|
||||
// processing
|
||||
.route("/jobs", post(processing::enqueue_job))
|
||||
.route(
|
||||
"/jobs",
|
||||
get(processing::list_jobs).post(processing::enqueue_job),
|
||||
)
|
||||
.route("/jobs/{id}/start", post(processing::start_job))
|
||||
.route("/jobs/{id}/complete", post(processing::complete_job))
|
||||
.route("/jobs/{id}/fail", post(processing::fail_job))
|
||||
.route("/jobs/batches/{id}", get(processing::batch_progress))
|
||||
.route("/plugins", post(processing::manage_plugin))
|
||||
.route("/pipelines", post(processing::configure_pipeline))
|
||||
.route_layer(from_fn_with_state(state.clone(), require_auth))
|
||||
}
|
||||
|
||||
pub fn app_router() -> Router<AppState> {
|
||||
pub fn app_router(state: &AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/health", get(health::health))
|
||||
.nest("/api/v1", api_v1_router())
|
||||
.nest("/api/v1", public_routes().merge(protected_routes(state)))
|
||||
.merge(openapi_router())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user