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,49 +1,28 @@
|
||||
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
|
||||
use crate::{errors::AppError, extractors::JwtClaims, parsers, state::AppState};
|
||||
use api_types::{
|
||||
requests::{
|
||||
CompleteJobRequest, ConfigurePipelineRequest, EnqueueJobRequest, FailJobRequest,
|
||||
ManagePluginRequest,
|
||||
},
|
||||
responses::{BatchProgressResponse, JobResponse, PipelineResponse, PluginResponse},
|
||||
responses::{
|
||||
BatchProgressResponse, JobListResponse, JobResponse, PipelineResponse, PluginResponse,
|
||||
},
|
||||
};
|
||||
use application::processing::{
|
||||
CompleteJobCommand, ConfigurePipelineCommand, EnqueueJobCommand, FailJobCommand,
|
||||
CompleteJobCommand, ConfigurePipelineCommand, EnqueueJobCommand, FailJobCommand, ListJobsQuery,
|
||||
ManagePluginCommand, PipelineStepConfig, PluginAction, ReportBatchProgressQuery,
|
||||
StartJobCommand,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use domain::{
|
||||
entities::{JobType, PluginType},
|
||||
errors::DomainError,
|
||||
value_objects::{MetadataValue, StructuredData, SystemId},
|
||||
};
|
||||
|
||||
fn parse_job_type(s: &str) -> JobType {
|
||||
match s {
|
||||
"scan_directory" => JobType::ScanDirectory,
|
||||
"extract_metadata" => JobType::ExtractMetadata,
|
||||
"generate_derivative" => JobType::GenerateDerivative,
|
||||
"sync_sidecar" => JobType::SyncSidecar,
|
||||
"detect_duplicates" => JobType::DetectDuplicates,
|
||||
other => JobType::Custom(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_plugin_type(s: &str) -> Result<PluginType, AppError> {
|
||||
match s {
|
||||
"media_processor" => Ok(PluginType::MediaProcessor),
|
||||
"scheduled_task" => Ok(PluginType::ScheduledTask),
|
||||
"sidecar_writer" => Ok(PluginType::SidecarWriter),
|
||||
_ => Err(AppError::from(DomainError::Validation(format!(
|
||||
"Invalid plugin type: {s}"
|
||||
)))),
|
||||
}
|
||||
}
|
||||
|
||||
fn hashmap_to_structured(
|
||||
map: &std::collections::HashMap<String, serde_json::Value>,
|
||||
) -> StructuredData {
|
||||
@@ -54,11 +33,38 @@ fn hashmap_to_structured(
|
||||
sd
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ListJobsParams {
|
||||
pub status: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
|
||||
pub async fn list_jobs(
|
||||
State(state): State<AppState>,
|
||||
claims: JwtClaims,
|
||||
Query(params): Query<ListJobsParams>,
|
||||
) -> Result<Json<JobListResponse>, AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let query = ListJobsQuery {
|
||||
status: params.status,
|
||||
limit: params.limit.unwrap_or(20).min(100),
|
||||
offset: params.offset.unwrap_or(0),
|
||||
};
|
||||
let result = state.processing.list_jobs.execute(query).await?;
|
||||
let jobs = result.jobs.iter().map(JobResponse::from_domain).collect();
|
||||
Ok(Json(JobListResponse {
|
||||
jobs,
|
||||
total: result.total,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn enqueue_job(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Json(req): Json<EnqueueJobRequest>,
|
||||
) -> Result<(StatusCode, Json<JobResponse>), AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let payload = req
|
||||
.payload
|
||||
.as_ref()
|
||||
@@ -66,7 +72,7 @@ pub async fn enqueue_job(
|
||||
.unwrap_or_default();
|
||||
|
||||
let cmd = EnqueueJobCommand {
|
||||
job_type: parse_job_type(&req.job_type),
|
||||
job_type: parsers::job_type(&req.job_type),
|
||||
priority: req.priority.unwrap_or(0),
|
||||
payload,
|
||||
target_asset_id: req.target_asset_id.map(SystemId::from_uuid),
|
||||
@@ -78,9 +84,10 @@ pub async fn enqueue_job(
|
||||
|
||||
pub async fn start_job(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Path((job_id,)): Path<(uuid::Uuid,)>,
|
||||
) -> Result<Json<JobResponse>, AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let cmd = StartJobCommand {
|
||||
job_id: SystemId::from_uuid(job_id),
|
||||
};
|
||||
@@ -90,10 +97,11 @@ pub async fn start_job(
|
||||
|
||||
pub async fn complete_job(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Path((job_id,)): Path<(uuid::Uuid,)>,
|
||||
Json(req): Json<CompleteJobRequest>,
|
||||
) -> Result<Json<JobResponse>, AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let cmd = CompleteJobCommand {
|
||||
job_id: SystemId::from_uuid(job_id),
|
||||
result: hashmap_to_structured(&req.result),
|
||||
@@ -104,10 +112,11 @@ pub async fn complete_job(
|
||||
|
||||
pub async fn fail_job(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Path((job_id,)): Path<(uuid::Uuid,)>,
|
||||
Json(req): Json<FailJobRequest>,
|
||||
) -> Result<Json<JobResponse>, AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let cmd = FailJobCommand {
|
||||
job_id: SystemId::from_uuid(job_id),
|
||||
error: req.error,
|
||||
@@ -118,9 +127,10 @@ pub async fn fail_job(
|
||||
|
||||
pub async fn batch_progress(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Path((batch_id,)): Path<(uuid::Uuid,)>,
|
||||
) -> Result<Json<BatchProgressResponse>, AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let query = ReportBatchProgressQuery {
|
||||
batch_id: SystemId::from_uuid(batch_id),
|
||||
};
|
||||
@@ -130,16 +140,17 @@ pub async fn batch_progress(
|
||||
|
||||
pub async fn manage_plugin(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Json(req): Json<ManagePluginRequest>,
|
||||
) -> Result<(StatusCode, Json<PluginResponse>), AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let action = match req.action.as_str() {
|
||||
"create" => {
|
||||
let name = req.name.ok_or_else(|| {
|
||||
AppError::from(DomainError::Validation("name required for create".into()))
|
||||
})?;
|
||||
let pt = req.plugin_type.as_deref().unwrap_or("media_processor");
|
||||
let plugin_type = parse_plugin_type(pt)?;
|
||||
let plugin_type = parsers::plugin_type(pt)?;
|
||||
let config = req
|
||||
.config
|
||||
.as_ref()
|
||||
@@ -173,9 +184,10 @@ pub async fn manage_plugin(
|
||||
|
||||
pub async fn configure_pipeline(
|
||||
State(state): State<AppState>,
|
||||
_claims: JwtClaims,
|
||||
claims: JwtClaims,
|
||||
Json(req): Json<ConfigurePipelineRequest>,
|
||||
) -> Result<(StatusCode, Json<PipelineResponse>), AppError> {
|
||||
super::require_admin(&claims)?;
|
||||
let steps = req
|
||||
.steps
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user