structural refactor and codebase improvements

This commit is contained in:
2026-08-09 14:58:14 +02:00
parent 22b1dd3f56
commit c9715baab8
247 changed files with 11515 additions and 3063 deletions

View File

@@ -7,11 +7,7 @@ use axum::{
use chrono::Utc;
use application::auth::{
commands::RegisterCommand,
deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps},
login as login_uc,
queries::LoginCommand,
register as register_uc,
commands::RegisterCommand, login as login_uc, queries::LoginCommand, register as register_uc,
};
use crate::{
@@ -56,15 +52,8 @@ pub async fn login(
State(state): State<AppState>,
Json(req): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, ApiError> {
let deps = LoginDeps {
user: state.app_ctx.repos.user.clone(),
password_hasher: state.app_ctx.services.password_hasher.clone(),
auth: state.app_ctx.services.auth.clone(),
refresh_session: state.app_ctx.repos.refresh_session.clone(),
config: state.app_ctx.config.clone(),
};
let result = login_uc::execute(
&deps,
&state.app_ctx.deps.auth.login,
LoginCommand {
email: req.email,
password: req.password,
@@ -93,13 +82,8 @@ pub async fn register(
State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
) -> Result<StatusCode, ApiError> {
let deps = RegisterDeps {
user: state.app_ctx.repos.user.clone(),
password_hasher: state.app_ctx.services.password_hasher.clone(),
config: state.app_ctx.config.clone(),
};
register_uc::execute(
&deps,
&state.app_ctx.deps.auth.register,
RegisterCommand {
email: req.email,
username: req.username,
@@ -123,12 +107,9 @@ pub async fn refresh(
State(state): State<AppState>,
Json(req): Json<RefreshRequest>,
) -> Result<Json<RefreshResponse>, ApiError> {
let deps = RefreshDeps {
refresh_session: state.app_ctx.repos.refresh_session.clone(),
auth: state.app_ctx.services.auth.clone(),
config: state.app_ctx.config.clone(),
};
let result = application::auth::refresh::execute(&deps, &req.refresh_token).await?;
let result =
application::auth::refresh::execute(&state.app_ctx.deps.auth.refresh, &req.refresh_token)
.await?;
Ok(Json(RefreshResponse {
token: result.token,
refresh_token: result.refresh_token,
@@ -147,11 +128,8 @@ pub async fn api_logout(
State(state): State<AppState>,
Json(req): Json<LogoutRequest>,
) -> StatusCode {
let _ = application::auth::logout::execute(
state.app_ctx.repos.refresh_session.clone(),
&req.refresh_token,
)
.await;
let _ = application::auth::logout::execute(&state.app_ctx.deps.auth.logout, &req.refresh_token)
.await;
StatusCode::NO_CONTENT
}
@@ -172,6 +150,7 @@ pub async fn get_login_page(
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
csrf_token: csrf.0,
page_rss_url: None,
pending_follow_count: 0,
};
render_page(LoginTemplate {
ctx: &ctx,
@@ -187,15 +166,8 @@ pub async fn post_login(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = LoginDeps {
user: state.app_ctx.repos.user.clone(),
password_hasher: state.app_ctx.services.password_hasher.clone(),
auth: state.app_ctx.services.auth.clone(),
refresh_session: state.app_ctx.repos.refresh_session.clone(),
config: state.app_ctx.config.clone(),
};
match login_uc::execute(
&deps,
&state.app_ctx.deps.auth.login,
LoginCommand {
email: form.email,
password: form.password,
@@ -242,6 +214,7 @@ pub async fn get_register_page(
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
csrf_token: csrf.0,
page_rss_url: None,
pending_follow_count: 0,
};
render_page(RegisterTemplate {
ctx: &ctx,
@@ -261,15 +234,8 @@ pub async fn post_register(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = RegisterAndLoginDeps {
user: state.app_ctx.repos.user.clone(),
password_hasher: state.app_ctx.services.password_hasher.clone(),
auth: state.app_ctx.services.auth.clone(),
refresh_session: state.app_ctx.repos.refresh_session.clone(),
config: state.app_ctx.config.clone(),
};
match application::auth::register_and_login::execute(
&deps,
&state.app_ctx.deps.auth.register_and_login,
application::auth::commands::RegisterAndLoginCommand {
email: form.email,
username: form.username,

View File

@@ -8,9 +8,7 @@ use uuid::Uuid;
use application::diary::{
commands::{DeleteReviewCommand, EditReviewCommand},
delete_review,
deps::{DeleteReviewDeps, EditReviewDeps, GetActivityFeedDeps},
edit_review, get_activity_feed as get_feed_uc, get_diary, log_review,
delete_review, edit_review, get_activity_feed as get_feed_uc, get_diary, log_review,
queries::GetActivityFeedQuery,
};
@@ -45,7 +43,8 @@ pub async fn get_diary(
State(state): State<AppState>,
Query(params): Query<DiaryQueryParams>,
) -> Result<Json<DiaryResponse>, ApiError> {
let page = get_diary::execute(&state.app_ctx.repos.diary, to_diary_query(params)).await?;
let page =
get_diary::execute(&state.app_ctx.deps.diary.get_diary, to_diary_query(params)).await?;
Ok(Json(DiaryResponse {
items: page
@@ -103,13 +102,7 @@ pub async fn delete_review(
review_id,
requesting_user_id: user_id.value(),
};
let deps = DeleteReviewDeps {
review: state.app_ctx.repos.review.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_command: state.app_ctx.repos.movie_command.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
delete_review::execute(&deps, cmd).await?;
delete_review::execute(&state.app_ctx.deps.diary.delete_review, cmd).await?;
Ok(StatusCode::NO_CONTENT)
}
@@ -145,11 +138,7 @@ pub async fn patch_review(
watched_at,
watch_medium: req.watch_medium,
};
let deps = EditReviewDeps {
review: state.app_ctx.repos.review.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
edit_review::execute(&deps, cmd).await?;
edit_review::execute(&state.app_ctx.deps.diary.edit_review, cmd).await?;
Ok(StatusCode::OK)
}
@@ -180,13 +169,8 @@ pub async fn get_activity_feed(
State(state): State<AppState>,
Query(params): Query<ActivityFeedQueryParams>,
) -> Result<Json<ActivityFeedResponse>, ApiError> {
let deps = GetActivityFeedDeps {
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query_unified.clone(),
config: state.app_ctx.config.clone(),
};
let page = get_feed_uc::execute(
&deps,
&state.app_ctx.deps.diary.get_activity_feed,
GetActivityFeedQuery {
limit: params.limit.unwrap_or(20),
offset: params.offset.unwrap_or(0),
@@ -274,13 +258,7 @@ pub async fn post_delete_review_html(
review_id,
requesting_user_id: user_id.value(),
};
let deps = DeleteReviewDeps {
review: state.app_ctx.repos.review.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_command: state.app_ctx.repos.movie_command.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match delete_review::execute(&deps, cmd).await {
match delete_review::execute(&state.app_ctx.deps.diary.delete_review, cmd).await {
Ok(()) => {
let redirect_url = form
.redirect_after
@@ -335,13 +313,12 @@ pub async fn get_activity_feed_html(
filter_following,
};
let deps = GetActivityFeedDeps {
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query_unified.clone(),
config: state.app_ctx.config.clone(),
};
match application::diary::get_activity_feed::execute(&deps, query).await {
match application::diary::get_activity_feed::execute(
&state.app_ctx.deps.diary.get_activity_feed,
query,
)
.await
{
Ok(entries) => {
let entry_limit = entries.limit;
let entry_offset = entries.offset;

View File

@@ -10,8 +10,6 @@ use api_types::{
CreateGoalRequest, GoalDto, GoalsResponse, UpdateGoalRequest, UpdateUserSettingsRequest,
UserSettingsDto,
};
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
// ── Shared mapper ────────────────────────────────────────────────────────────
pub fn goal_with_progress_to_dto(g: &domain::models::GoalWithProgress) -> GoalDto {
@@ -39,12 +37,8 @@ pub async fn list_goals(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<GoalsResponse>, ApiError> {
let deps = GoalQueryDeps {
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
};
let goals = application::goals::list::execute(
&deps,
&state.app_ctx.deps.goals.query,
application::goals::queries::ListGoalsQuery {
user_id: user.0.value(),
},
@@ -69,14 +63,8 @@ pub async fn create_goal(
user: AuthenticatedUser,
Json(req): Json<CreateGoalRequest>,
) -> Result<Json<GoalDto>, ApiError> {
let deps = GoalCommandDeps {
goal_command: state.app_ctx.repos.goal_command.clone(),
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
let g = application::goals::create::execute(
&deps,
&state.app_ctx.deps.goals.command,
application::goals::commands::CreateGoalCommand {
user_id: user.0.value(),
year: req.year,
@@ -103,14 +91,8 @@ pub async fn update_goal(
Path(year): Path<u16>,
Json(req): Json<UpdateGoalRequest>,
) -> Result<Json<GoalDto>, ApiError> {
let deps = GoalCommandDeps {
goal_command: state.app_ctx.repos.goal_command.clone(),
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
let g = application::goals::update::execute(
&deps,
&state.app_ctx.deps.goals.command,
application::goals::commands::UpdateGoalCommand {
user_id: user.0.value(),
year,
@@ -135,14 +117,8 @@ pub async fn delete_goal(
user: AuthenticatedUser,
Path(year): Path<u16>,
) -> Result<StatusCode, ApiError> {
let deps = GoalCommandDeps {
goal_command: state.app_ctx.repos.goal_command.clone(),
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
application::goals::delete::execute(
&deps,
&state.app_ctx.deps.goals.command,
application::goals::commands::DeleteGoalCommand {
user_id: user.0.value(),
year,
@@ -165,12 +141,8 @@ pub async fn get_user_goals(
AuthenticatedUser(_viewer): AuthenticatedUser,
Path(user_id): Path<Uuid>,
) -> Result<Json<GoalsResponse>, ApiError> {
let deps = GoalQueryDeps {
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
};
let goals = application::goals::list::execute(
&deps,
&state.app_ctx.deps.goals.query,
application::goals::queries::ListGoalsQuery { user_id },
)
.await?;
@@ -194,7 +166,7 @@ pub async fn get_settings(
user: AuthenticatedUser,
) -> Result<Json<UserSettingsDto>, ApiError> {
let settings = application::users::get_settings::execute(
state.app_ctx.repos.user_settings.clone(),
&state.app_ctx.deps.users.get_settings,
user.0.value(),
)
.await?;
@@ -220,7 +192,7 @@ pub async fn update_settings(
Json(req): Json<UpdateUserSettingsRequest>,
) -> Result<StatusCode, ApiError> {
application::users::update_settings::execute(
state.app_ctx.repos.user_settings.clone(),
&state.app_ctx.deps.users.update_settings,
application::users::update_settings::UpdateUserSettingsCommand {
user_id: user.0.value(),
federate_goals: req.federate_goals,

View File

@@ -29,11 +29,7 @@ pub(crate) fn build_export_response(
ExportFormat::Json => ("application/json", "diary.json"),
};
let query = ExportQuery { user_id, format };
let stream = export_diary_uc::execute(
&state.app_ctx.repos.diary,
&state.app_ctx.services.diary_exporter,
query,
);
let stream = export_diary_uc::execute(&state.app_ctx.deps.diary.export_diary, query);
let stream = stream.map(|r| {
if let Err(ref e) = r {
tracing::error!("diary export stream error: {e}");
@@ -146,16 +142,25 @@ pub(crate) async fn build_page_context(
csrf_token: String,
) -> HtmlPageContext {
let uuid = user_id.as_ref().map(|u| u.value());
let (user_email, is_admin) = if let Some(ref id) = user_id {
let user = state.app_ctx.repos.user.find_by_id(id).await.ok().flatten();
let email = user.as_ref().map(|u| u.email().value().to_string());
let admin = user
.as_ref()
.map(|u| matches!(u.role(), domain::models::UserRole::Admin))
.unwrap_or(false);
(email, admin)
// Page chrome (email/admin badge, pending-follow badge) degrades rather than
// blanking the page: a failed lookup or count logs a warning and falls back
// to a zeroed `PageViewer`, same as the previous silent `.unwrap_or(0)` — now
// logged and explicit, per the error policy in `get_page_viewer`'s doc comment.
let (user_email, is_admin, pending_follow_count) = if let Some(ref id) = user_id {
match application::users::get_page_viewer::execute(
&state.app_ctx.deps.users.get_page_viewer,
id.value(),
)
.await
{
Ok(viewer) => (viewer.email, viewer.is_admin, viewer.pending_follow_count),
Err(e) => {
tracing::warn!("get_page_viewer failed for {}: {e}", id.value());
(None, false, 0)
}
}
} else {
(None, false)
(None, false, 0)
};
HtmlPageContext {
user_email,
@@ -167,5 +172,6 @@ pub(crate) async fn build_page_context(
canonical_url: state.app_ctx.config.base_url.clone(),
csrf_token,
page_rss_url: None,
pending_follow_count,
}
}

View File

@@ -13,17 +13,14 @@ use std::collections::HashMap;
use crate::render::render_page;
use application::import::{
apply_mapping as apply_import_mapping, apply_profile as apply_import_profile,
apply_mapping as apply_import_mapping, apply_profile_and_map,
commands::{
ApplyImportMappingCommand, ApplyImportProfileCommand, CreateImportSessionCommand,
ApplyImportMappingCommand, ApplyProfileAndMapCommand, CreateImportSessionCommand,
DeleteImportProfileCommand, ExecuteImportCommand, SaveImportProfileCommand,
},
create_session as create_import_session, delete_profile as delete_import_profile,
deps::{
ApplyMappingDeps, ApplyProfileDeps, CreateSessionDeps, ExecuteImportDeps, SaveProfileDeps,
},
execute as execute_import, list_profiles as list_import_profiles,
save_profile as save_import_profile,
execute as execute_import, get_mapping_stage, get_preview_stage, get_session_state,
list_profiles as list_import_profiles, save_profile as save_import_profile,
};
use domain::errors::DomainError;
use domain::models::{
@@ -111,7 +108,7 @@ pub async fn get_import_page(
) -> impl IntoResponse {
let ctx = super::helpers::build_page_context(&state, Some(user_id.clone()), csrf.0).await;
let profiles =
list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id)
list_import_profiles::execute(&state.app_ctx.deps.import.list_profiles, &user_id)
.await
.unwrap_or_default()
.into_iter()
@@ -163,10 +160,7 @@ pub async fn post_upload(
};
match create_import_session::execute(
&CreateSessionDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
},
&state.app_ctx.deps.import.create_session,
CreateImportSessionCommand {
user_id: user_id.value(),
bytes,
@@ -194,21 +188,17 @@ pub async fn get_mapping_page(
else {
return Redirect::to("/import").into_response();
};
let Ok(Some(session)) = state
.app_ctx
.repos
.import_session
.get(&session_id, &user_id)
.await
let Ok(stage) = get_mapping_stage::execute(
&state.app_ctx.deps.import.get_mapping_stage,
session_id,
user_id.value(),
)
.await
else {
return Redirect::to("/import").into_response();
};
let Some(parsed) = session.parsed_file else {
return Redirect::to("/import").into_response();
};
let ctx = super::helpers::build_page_context(&state, Some(user_id), csrf.0).await;
let sample_rows: Vec<Vec<String>> = parsed.rows.into_iter().take(5).collect();
let domain_fields: Vec<(&str, &str)> = vec![
("title", "Title"),
("release_year", "Release Year"),
@@ -221,8 +211,8 @@ pub async fn get_mapping_page(
render_page(ImportMappingTemplate {
ctx: &ctx,
session_id: &session_id_str,
columns: &parsed.columns,
sample_rows: &sample_rows,
columns: &stage.columns,
sample_rows: &stage.sample_rows,
domain_fields: &domain_fields,
error: None,
})
@@ -255,11 +245,7 @@ pub async fn post_mapping(
.into_response();
}
match apply_import_mapping::execute(
&ApplyMappingDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
},
&state.app_ctx.deps.import.apply_mapping,
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -290,24 +276,25 @@ pub async fn get_preview_page(
else {
return Redirect::to("/import").into_response();
};
let Ok(Some(session)) = state
.app_ctx
.repos
.import_session
.get(&session_id, &user_id)
.await
let Ok(stage) = get_preview_stage::execute(
&state.app_ctx.deps.import.get_preview_stage,
session_id,
user_id.value(),
)
.await
else {
return Redirect::to("/import").into_response();
};
if session.row_results.is_none() {
return Redirect::to(&format!("/import/{}/mapping", session_id_str)).into_response();
}
let preview = match stage {
get_preview_stage::PreviewStage::Ready(preview) => preview,
get_preview_stage::PreviewStage::NotYetMapped => {
return Redirect::to(&format!("/import/{}/mapping", session_id_str)).into_response();
}
};
let parsed = session.parsed_file.unwrap_or_default();
let annotated: Vec<AnnotatedRow> = session.row_results.unwrap_or_default();
let rows: Vec<ImportPreviewRow> = annotated
let rows: Vec<ImportPreviewRow> = preview
.rows
.iter()
.enumerate()
.map(|(i, a)| annotated_to_preview_row(i, a))
@@ -317,7 +304,7 @@ pub async fn get_preview_page(
render_page(ImportPreviewTemplate {
ctx: &ctx,
session_id: &session_id_str,
columns: &parsed.columns,
columns: &preview.columns,
rows: &rows,
})
.into_response()
@@ -353,10 +340,7 @@ pub async fn post_confirm(
.filter(|n| !n.trim().is_empty());
if let Some(name) = profile_name {
let _ = save_import_profile::execute(
&SaveProfileDeps {
import_session: state.app_ctx.repos.import_session.clone(),
import_profile: state.app_ctx.repos.import_profile.clone(),
},
&state.app_ctx.deps.import.save_profile,
SaveImportProfileCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -374,10 +358,7 @@ pub async fn post_confirm(
.collect();
match execute_import::execute(
&ExecuteImportDeps {
import_session: state.app_ctx.repos.import_session.clone(),
review_logger: state.app_ctx.services.review_logger.clone(),
},
&state.app_ctx.deps.import.execute_import,
ExecuteImportCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -412,7 +393,7 @@ pub async fn post_delete_profile(
}
if let Ok(profile_id) = profile_id_str.parse::<uuid::Uuid>() {
let _ = delete_import_profile::execute(
state.app_ctx.repos.import_profile.clone(),
&state.app_ctx.deps.import.delete_profile,
DeleteImportProfileCommand {
user_id: user_id.value(),
profile_id,
@@ -498,10 +479,7 @@ pub async fn api_post_session(
_ => FileFormat::Csv,
};
let r = create_import_session::execute(
&CreateSessionDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
},
&state.app_ctx.deps.import.create_session,
CreateImportSessionCommand {
user_id: user_id.value(),
bytes,
@@ -535,20 +513,17 @@ pub async fn api_get_session(
.parse::<uuid::Uuid>()
.map(ImportSessionId::from_uuid)
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
let session = state
.app_ctx
.repos
.import_session
.get(&session_id, &user_id)
.await?
.ok_or(DomainError::NotFound("session not found".into()))?;
let parsed = session.parsed_file.unwrap_or_default();
let row_count = parsed.rows.len();
let state_data = get_session_state::execute(
&state.app_ctx.deps.import.get_session_state,
session_id,
user_id.value(),
)
.await?;
Ok(axum::Json(SessionStateResponse {
session_id: session_id_str,
columns: parsed.columns,
has_mappings: session.field_mappings.is_some(),
row_count,
columns: state_data.columns,
has_mappings: state_data.has_mappings,
row_count: state_data.row_count,
}))
}
@@ -596,11 +571,7 @@ pub async fn api_put_mapping(
.collect();
let rows = apply_import_mapping::execute(
&ApplyMappingDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
},
&state.app_ctx.deps.import.apply_mapping,
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -621,15 +592,21 @@ pub async fn api_get_preview(
.map(ImportSessionId::from_uuid)
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
let session = state
.app_ctx
.repos
.import_session
.get(&session_id, &user_id)
.await?
.ok_or(DomainError::NotFound("session not found".into()))?;
let stage = get_preview_stage::execute(
&state.app_ctx.deps.import.get_preview_stage,
session_id,
user_id.value(),
)
.await?;
let annotated: Vec<AnnotatedRow> = session.row_results.unwrap_or_default();
// Not-yet-mapped sessions render as an empty preview here (200, `rows: []`),
// matching this endpoint's behavior before the stage-gate moved into
// `get_preview_stage` — unlike the HTML handler, this API route never
// redirected on the same condition, so its response shape is unchanged.
let annotated: Vec<AnnotatedRow> = match stage {
get_preview_stage::PreviewStage::Ready(preview) => preview.rows,
get_preview_stage::PreviewStage::NotYetMapped => Vec::new(),
};
let rows = annotated
.iter()
.enumerate()
@@ -689,10 +666,7 @@ pub async fn api_post_confirm(
.map(ImportSessionId::from_uuid)
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
let s = execute_import::execute(
&ExecuteImportDeps {
import_session: state.app_ctx.repos.import_session.clone(),
review_logger: state.app_ctx.services.review_logger.clone(),
},
&state.app_ctx.deps.import.execute_import,
ExecuteImportCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -720,7 +694,7 @@ pub async fn api_get_profiles(
AuthenticatedUser(user_id): AuthenticatedUser,
) -> Result<impl IntoResponse, ApiError> {
let profiles =
list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id).await?;
list_import_profiles::execute(&state.app_ctx.deps.import.list_profiles, &user_id).await?;
Ok(axum::Json(
profiles
.into_iter()
@@ -756,10 +730,7 @@ pub async fn api_post_profile(
.map(ImportSessionId::from_uuid)
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
let id = save_import_profile::execute(
&SaveProfileDeps {
import_session: state.app_ctx.repos.import_session.clone(),
import_profile: state.app_ctx.repos.import_profile.clone(),
},
&state.app_ctx.deps.import.save_profile,
SaveImportProfileCommand {
user_id: user_id.value(),
session_id: session_id.value(),
@@ -791,7 +762,7 @@ pub async fn api_delete_profile(
.parse::<uuid::Uuid>()
.map_err(|_| DomainError::ValidationError("invalid profile id".into()))?;
delete_import_profile::execute(
state.app_ctx.repos.import_profile.clone(),
&state.app_ctx.deps.import.delete_profile,
DeleteImportProfileCommand {
user_id: user_id.value(),
profile_id,
@@ -828,42 +799,14 @@ pub async fn api_apply_profile(
.parse::<uuid::Uuid>()
.map_err(|_| DomainError::ValidationError("invalid profile id".into()))?;
apply_import_profile::execute(
&ApplyProfileDeps {
import_profile: state.app_ctx.repos.import_profile.clone(),
import_session: state.app_ctx.repos.import_session.clone(),
},
ApplyImportProfileCommand {
let rows = apply_profile_and_map::execute(
&state.app_ctx.deps.import.apply_profile_and_map,
ApplyProfileAndMapCommand {
user_id: user_id.value(),
session_id,
profile_id,
},
)
.await?;
let session = state
.app_ctx
.repos
.import_session
.get(&ImportSessionId::from_uuid(session_id), &user_id)
.await?
.ok_or(DomainError::NotFound(
"session not found after profile apply".into(),
))?;
let mappings = session.field_mappings.unwrap_or_default();
let rows = apply_import_mapping::execute(
&ApplyMappingDeps {
import_session: state.app_ctx.repos.import_session.clone(),
document_parser: state.app_ctx.services.document_parser.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
},
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id,
mappings,
},
)
.await?;
Ok(axum::Json(serde_json::json!({"row_count": rows.len()})))
}

View File

@@ -41,9 +41,10 @@ pub async fn get_integrations_page(
let query = GetWebhookTokensQuery {
user_id: user_id.value(),
};
let tokens = get_webhook_tokens::execute(state.app_ctx.repos.webhook_token.clone(), query)
.await
.unwrap_or_default();
let tokens =
get_webhook_tokens::execute(&state.app_ctx.deps.integrations.get_webhook_tokens, query)
.await
.unwrap_or_default();
let token_views: Vec<template_askama::WebhookTokenView> = tokens
.iter()
@@ -81,7 +82,12 @@ pub async fn post_generate_token(
label: form.label.filter(|l| !l.trim().is_empty()),
};
match generate_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await {
match generate_webhook_token::execute(
&state.app_ctx.deps.integrations.generate_webhook_token,
cmd,
)
.await
{
Ok(result) => {
let encoded = percent_encoding::utf8_percent_encode(
&result.token_plaintext,
@@ -112,7 +118,8 @@ pub async fn post_revoke_token(
token_id,
};
if let Err(e) =
revoke_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await
revoke_webhook_token::execute(&state.app_ctx.deps.integrations.revoke_webhook_token, cmd)
.await
{
tracing::error!("revoke token failed: {:?}", e);
}
@@ -135,7 +142,7 @@ pub async fn get_watch_queue_page(
let query = GetWatchQueueQuery {
user_id: user_id.value(),
};
let events = get_watch_queue::execute(state.app_ctx.repos.watch_event_query.clone(), query)
let events = get_watch_queue::execute(&state.app_ctx.deps.integrations.get_watch_queue, query)
.await
.unwrap_or_default();
@@ -172,13 +179,8 @@ pub async fn post_confirm_single(
}],
};
match confirm_watch_events::execute(
state.app_ctx.repos.watch_event_command.clone(),
state.app_ctx.repos.watch_event_query.clone(),
state.app_ctx.services.review_logger.clone(),
cmd,
)
.await
match confirm_watch_events::execute(&state.app_ctx.deps.integrations.confirm_watch_events, cmd)
.await
{
Ok(_) => Redirect::to("/watch-queue").into_response(),
Err(e) => {
@@ -204,12 +206,8 @@ pub async fn post_dismiss_single(
event_ids: vec![event_id],
};
match dismiss_watch_events::execute(
state.app_ctx.repos.watch_event_command.clone(),
state.app_ctx.repos.watch_event_query.clone(),
cmd,
)
.await
match dismiss_watch_events::execute(&state.app_ctx.deps.integrations.dismiss_watch_events, cmd)
.await
{
Ok(_) => Redirect::to("/watch-queue").into_response(),
Err(e) => {

View File

@@ -8,7 +8,6 @@ pub mod integrations;
pub mod movies;
pub mod rss;
pub mod search;
#[cfg(feature = "federation")]
pub mod social;
pub mod users;
pub mod watchlist;

View File

@@ -9,11 +9,10 @@ use uuid::Uuid;
use application::{
diary::{
commands::SyncPosterCommand,
deps::GetMovieSocialPageDeps,
get_movie_social_page, get_review_history,
queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery},
},
movies::{deps::SyncPosterDeps, get_movies, queries::GetMoviesQuery, sync_poster},
movies::{get_movies, queries::GetMoviesQuery, sync_poster},
watchlist::{is_on as is_on_watchlist, queries::IsOnWatchlistQuery},
};
use domain::services::review_history::Trend;
@@ -48,7 +47,7 @@ pub async fn list_movies(
Query(params): Query<MoviesQueryParams>,
) -> Result<Json<MoviesResponse>, ApiError> {
let page = get_movies::execute(
state.app_ctx.repos.movie_query.clone(),
&state.app_ctx.deps.movies.get_movies,
GetMoviesQuery {
limit: params.limit,
offset: params.offset,
@@ -84,7 +83,7 @@ pub async fn get_review_history(
Path(movie_id): Path<Uuid>,
) -> Result<Json<ReviewHistoryResponse>, ApiError> {
let (history, trend) = get_review_history::execute(
&state.app_ctx.repos.diary,
&state.app_ctx.deps.diary.get_review_history,
GetReviewHistoryQuery { movie_id },
)
.await?;
@@ -121,16 +120,7 @@ pub async fn sync_poster(
Path(movie_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
sync_poster::execute(
&SyncPosterDeps {
movie_command: state.app_ctx.repos.movie_command.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
metadata: state.app_ctx.services.metadata.clone(),
poster_fetcher: state.app_ctx.services.poster_fetcher.clone(),
object_storage: state.app_ctx.services.object_storage.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
search_command: state.app_ctx.repos.search_command.clone(),
},
&state.app_ctx.deps.movies.sync_poster,
SyncPosterCommand { movie_id },
)
.await?;
@@ -154,11 +144,7 @@ pub async fn get_movie_detail(
let offset = params.offset.unwrap_or(0);
let result = get_movie_social_page::execute(
&GetMovieSocialPageDeps {
movie_query: state.app_ctx.repos.movie_query.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
},
&state.app_ctx.deps.diary.get_movie_social_page,
GetMovieSocialPageQuery {
movie_id,
limit,
@@ -210,7 +196,7 @@ pub async fn get_movie_profile(
) -> impl IntoResponse {
use application::movies::get_movie_profile;
let query = get_movie_profile::GetMovieProfileQuery { movie_id };
match get_movie_profile::execute(state.app_ctx.repos.movie_profile.clone(), query).await {
match get_movie_profile::execute(&state.app_ctx.deps.movies.get_movie_profile, query).await {
Ok(Some(result)) => {
let p = result.profile;
Json(MovieProfileResponse {
@@ -288,11 +274,7 @@ pub async fn get_movie_detail_html(
let offset = params.offset.unwrap_or(0);
match get_movie_social_page::execute(
&GetMovieSocialPageDeps {
movie_query: state.app_ctx.repos.movie_query.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
},
&state.app_ctx.deps.diary.get_movie_social_page,
GetMovieSocialPageQuery {
movie_id,
limit,
@@ -314,7 +296,7 @@ pub async fn get_movie_detail_html(
result.reviews.offset + result.reviews.limit < result.reviews.total_count as u32;
let on_watchlist = match &user_id {
Some(uid) => is_on_watchlist::execute(
state.app_ctx.repos.watchlist.clone(),
&state.app_ctx.deps.watchlist.is_on_watchlist,
IsOnWatchlistQuery {
user_id: uid.value(),
movie_id,

View File

@@ -5,8 +5,8 @@ use axum::{
};
use uuid::Uuid;
use application::{diary::get_diary, diary::queries::GetDiaryQuery};
use domain::{errors::DomainError, models::ReviewSortBy, value_objects::UserId};
use application::{diary::get_diary, diary::get_user_feed, diary::queries::GetDiaryQuery};
use domain::{errors::DomainError, models::ReviewSortBy};
use crate::{errors::ApiError, state::AppState};
@@ -18,7 +18,7 @@ pub async fn get_feed(State(state): State<AppState>) -> Result<impl IntoResponse
movie_id: None,
user_id: None,
};
let page = get_diary::execute(&state.app_ctx.repos.diary, query).await?;
let page = get_diary::execute(&state.app_ctx.deps.diary.get_diary, query).await?;
let xml = state
.rss_renderer
.render_feed(&page.items, "Movie Diary")
@@ -33,30 +33,18 @@ pub async fn get_user_feed(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
let user = state
.app_ctx
.repos
.user
.find_by_id(&UserId::from_uuid(user_id))
.await
.map_err(ApiError)?
.ok_or_else(|| ApiError(DomainError::NotFound(format!("User {user_id}"))))?;
let feed = get_user_feed::execute(
&state.app_ctx.deps.diary.get_user_feed,
user_id,
super::RSS_FEED_LIMIT,
)
.await?;
let query = GetDiaryQuery {
limit: Some(super::RSS_FEED_LIMIT),
offset: Some(0),
sort_by: Some(ReviewSortBy::Descending),
movie_id: None,
user_id: Some(user_id),
};
let page = get_diary::execute(&state.app_ctx.repos.diary, query).await?;
let display_name = user.email().value().split('@').next().unwrap_or("User");
let title = format!("{}'s Movie Diary", display_name);
let title = format!("{}'s Movie Diary", feed.author.display_name);
let xml = state
.rss_renderer
.render_feed(&page.items, &title)
.render_feed(&feed.entries, &title)
.map_err(|e| ApiError(DomainError::InfrastructureError(e)))?;
Ok((

View File

@@ -5,7 +5,7 @@ use axum::{
};
use application::{
person::{deps::GetPersonDeps, get as get_person, get_credits as get_person_credits},
person::{get as get_person, get_credits as get_person_credits},
search::execute as search_uc,
};
use domain::models::{PersonId, collections::PageParams};
@@ -45,7 +45,7 @@ pub async fn get_search(
},
};
match search_uc::execute(state.app_ctx.repos.search_port.clone(), query).await {
match search_uc::execute(&state.app_ctx.deps.search.execute, query).await {
Ok(results) => axum::Json(SearchResponse {
movies: PaginatedMovieHits {
items: results
@@ -101,11 +101,12 @@ pub async fn get_person_handler(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
) -> impl IntoResponse {
let deps = GetPersonDeps {
person_query: state.app_ctx.repos.person_query.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match get_person::execute(&deps, PersonId::from_uuid(id)).await {
match get_person::execute(
&state.app_ctx.deps.person.get_person,
PersonId::from_uuid(id),
)
.await
{
Ok(Some(person)) => {
axum::Json(crate::mappers::search::person_to_dto(&person)).into_response()
}
@@ -127,11 +128,12 @@ pub async fn get_person_credits_handler(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
) -> impl IntoResponse {
let deps = GetPersonDeps {
person_query: state.app_ctx.repos.person_query.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match get_person_credits::execute(&deps, PersonId::from_uuid(id)).await {
match get_person_credits::execute(
&state.app_ctx.deps.person.get_person,
PersonId::from_uuid(id),
)
.await
{
Ok(credits) => axum::Json(PersonCreditsDto {
person: crate::mappers::search::person_to_dto(&credits.person),
cast: credits

View File

@@ -9,77 +9,58 @@ use uuid::Uuid;
use crate::{
csrf::CsrfToken,
errors::ApiError,
extractors::{AdminApiUser, AuthenticatedUser, RequiredCookieUser},
forms::{
ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm,
UnfollowForm,
},
extractors::{AuthenticatedUser, RequiredCookieUser},
forms::{FollowForm, FollowerActionForm, UnfollowForm},
render::render_page,
state::AppState,
};
#[cfg(feature = "federation")]
use crate::{
extractors::AdminApiUser,
forms::{ActorUrlForm, BlockDomainForm, RemoveDomainForm},
};
use api_types::{
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
BlockedDomainResponse, FollowRequest, RemoteActorDto,
};
use application::social::deps::{SocialCommandDeps, SocialQueryDeps};
use domain::value_objects::{FollowTarget, SocialActor, SocialIdentity};
use template_askama::{
BlockedActorsTemplate, BlockedDomainsTemplate, FollowersTemplate, FollowingTemplate,
RemoteActorData,
ActorListResponse, ActorUrlRequest, FollowRelationResponse, FollowRequest,
PendingCountResponse, RemoteActorDto,
};
#[cfg(feature = "federation")]
use api_types::{AddBlockedDomainRequest, BlockedActorResponse, BlockedDomainResponse};
use domain::value_objects::{FollowTarget, InstanceIdentity, SocialActor, SocialIdentity};
#[cfg(feature = "federation")]
use template_askama::{BlockedActorsTemplate, BlockedDomainsTemplate};
use template_askama::{FollowersTemplate, FollowingTemplate, RemoteActorData};
use super::helpers::{build_page_context, encode_error};
impl From<&AppState> for SocialCommandDeps {
fn from(state: &AppState) -> Self {
Self {
social_command: state.app_ctx.repos.social_command.clone(),
social_query: state.app_ctx.repos.social_query_unified.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
}
}
}
impl From<&AppState> for SocialQueryDeps {
fn from(state: &AppState) -> Self {
Self {
social_query: state.app_ctx.repos.social_query_unified.clone(),
}
}
}
fn ap_to_domain(e: anyhow::Error) -> domain::errors::DomainError {
tracing::error!("ActivityPub error: {:?}", e);
domain::errors::DomainError::InfrastructureError(e.to_string())
}
fn actor_url(identity: &SocialIdentity) -> String {
match identity {
SocialIdentity::Remote { actor_url } => actor_url.clone(),
SocialIdentity::Local(uid) => format!("local:{}", uid.value()),
}
}
fn social_actor_to_dto(actor: SocialActor) -> RemoteActorDto {
fn social_actor_to_dto(actor: SocialActor, instance: &InstanceIdentity) -> RemoteActorDto {
RemoteActorDto {
url: actor_url(&actor.identity),
handle: actor.handle,
display_name: actor.display_name,
}
}
fn social_actor_to_blocked_dto(actor: SocialActor) -> BlockedActorResponse {
BlockedActorResponse {
url: actor_url(&actor.identity),
url: instance.actor_url_of(&actor.identity),
user_id: match &actor.identity {
SocialIdentity::Local(uid) => Some(uid.value()),
SocialIdentity::Remote { .. } => None,
},
handle: actor.handle,
display_name: actor.display_name,
avatar_url: actor.avatar_url,
}
}
fn social_actor_to_template(actor: SocialActor) -> RemoteActorData {
#[cfg(feature = "federation")]
fn social_actor_to_blocked_dto(
actor: SocialActor,
instance: &InstanceIdentity,
) -> BlockedActorResponse {
BlockedActorResponse {
url: instance.actor_url_of(&actor.identity),
handle: actor.handle,
display_name: actor.display_name,
avatar_url: actor.avatar_url,
}
}
fn social_actor_to_template(actor: SocialActor, instance: &InstanceIdentity) -> RemoteActorData {
RemoteActorData {
url: actor_url(&actor.identity),
url: instance.actor_url_of(&actor.identity),
handle: actor.handle,
display_name: actor.display_name,
avatar_url: actor.avatar_url,
@@ -88,6 +69,7 @@ fn social_actor_to_template(actor: SocialActor) -> RemoteActorData {
// ── API ──────────────────────────────────────────────────────────────────────
#[cfg(feature = "federation")]
#[utoipa::path(
get, path = "/api/v1/admin/blocked-domains",
responses(
@@ -101,13 +83,7 @@ pub async fn get_blocked_domains_admin(
State(state): State<AppState>,
_admin: AdminApiUser,
) -> Result<Json<Vec<BlockedDomainResponse>>, ApiError> {
let domains = state
.app_ctx
.services
.ap_service
.get_blocked_domains()
.await
.map_err(ap_to_domain)?;
let domains = state.app_ctx.ap_blocklist.get_blocked_domains().await?;
Ok(Json(
domains
.into_iter()
@@ -120,6 +96,7 @@ pub async fn get_blocked_domains_admin(
))
}
#[cfg(feature = "federation")]
#[utoipa::path(
post, path = "/api/v1/admin/blocked-domains",
request_body = AddBlockedDomainRequest,
@@ -137,14 +114,13 @@ pub async fn add_blocked_domain_admin(
) -> Result<impl IntoResponse, ApiError> {
state
.app_ctx
.services
.ap_service
.ap_blocklist
.add_blocked_domain(&body.domain, body.reason.as_deref())
.await
.map_err(ap_to_domain)?;
.await?;
Ok(StatusCode::CREATED)
}
#[cfg(feature = "federation")]
#[utoipa::path(
delete, path = "/api/v1/admin/blocked-domains/{domain}",
params(("domain" = String, Path, description = "Domain to unblock")),
@@ -162,14 +138,13 @@ pub async fn remove_blocked_domain_admin(
) -> Result<impl IntoResponse, ApiError> {
state
.app_ctx
.services
.ap_service
.ap_blocklist
.remove_blocked_domain(&domain)
.await
.map_err(ap_to_domain)?;
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[cfg(feature = "federation")]
#[utoipa::path(
post, path = "/api/v1/social/block",
request_body = ActorUrlRequest,
@@ -184,18 +159,19 @@ pub async fn block_actor_api(
user: AuthenticatedUser,
axum::Json(body): axum::Json<ActorUrlRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Block {
blocker_id: user.0.value(),
target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url),
target: instance.identify(&body.actor_url),
},
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[cfg(feature = "federation")]
#[utoipa::path(
post, path = "/api/v1/social/unblock",
request_body = ActorUrlRequest,
@@ -210,18 +186,19 @@ pub async fn unblock_actor_api(
user: AuthenticatedUser,
axum::Json(body): axum::Json<ActorUrlRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Unblock {
blocker_id: user.0.value(),
target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url),
target: instance.identify(&body.actor_url),
},
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[cfg(feature = "federation")]
#[utoipa::path(
get, path = "/api/v1/social/blocked",
responses(
@@ -234,18 +211,14 @@ pub async fn get_blocked_actors_api(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<Vec<BlockedActorResponse>>, ApiError> {
let deps = SocialQueryDeps::from(&state);
let identities = application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetBlocked {
user_id: user.0.value(),
},
)
.await?;
let instance = state.app_ctx.instance.clone();
let identities =
application::social::get_blocked::execute(&state.app_ctx.deps.social.query, user.0.value())
.await?;
Ok(Json(
identities
.into_iter()
.map(social_actor_to_blocked_dto)
.map(|a| social_actor_to_blocked_dto(a, &instance))
.collect(),
))
}
@@ -262,16 +235,17 @@ pub async fn get_following(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<ActorListResponse>, ApiError> {
let deps = SocialQueryDeps::from(&state);
let identities = application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetFollowing {
user_id: user.0.value(),
},
let instance = state.app_ctx.instance.clone();
let identities = application::social::get_following::execute(
&state.app_ctx.deps.social.query,
user.0.value(),
)
.await?;
Ok(Json(ActorListResponse {
actors: identities.into_iter().map(social_actor_to_dto).collect(),
actors: identities
.into_iter()
.map(|a| social_actor_to_dto(a, &instance))
.collect(),
}))
}
@@ -287,16 +261,17 @@ pub async fn get_followers(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<ActorListResponse>, ApiError> {
let deps = SocialQueryDeps::from(&state);
let identities = application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetFollowers {
user_id: user.0.value(),
},
let instance = state.app_ctx.instance.clone();
let identities = application::social::get_followers::execute(
&state.app_ctx.deps.social.query,
user.0.value(),
)
.await?;
Ok(Json(ActorListResponse {
actors: identities.into_iter().map(social_actor_to_dto).collect(),
actors: identities
.into_iter()
.map(|a| social_actor_to_dto(a, &instance))
.collect(),
}))
}
@@ -305,14 +280,15 @@ pub async fn get_user_following(
_user: AuthenticatedUser,
Path(user_id): Path<Uuid>,
) -> Result<Json<ActorListResponse>, ApiError> {
let deps = SocialQueryDeps::from(&state);
let identities = application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetFollowing { user_id },
)
.await?;
let instance = state.app_ctx.instance.clone();
let identities =
application::social::get_following::execute(&state.app_ctx.deps.social.query, user_id)
.await?;
Ok(Json(ActorListResponse {
actors: identities.into_iter().map(social_actor_to_dto).collect(),
actors: identities
.into_iter()
.map(|a| social_actor_to_dto(a, &instance))
.collect(),
}))
}
@@ -321,14 +297,15 @@ pub async fn get_user_followers(
_user: AuthenticatedUser,
Path(user_id): Path<Uuid>,
) -> Result<Json<ActorListResponse>, ApiError> {
let deps = SocialQueryDeps::from(&state);
let identities = application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetFollowers { user_id },
)
.await?;
let instance = state.app_ctx.instance.clone();
let identities =
application::social::get_followers::execute(&state.app_ctx.deps.social.query, user_id)
.await?;
Ok(Json(ActorListResponse {
actors: identities.into_iter().map(social_actor_to_dto).collect(),
actors: identities
.into_iter()
.map(|a| social_actor_to_dto(a, &instance))
.collect(),
}))
}
@@ -346,9 +323,8 @@ pub async fn follow(
user: AuthenticatedUser,
Json(body): Json<FollowRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Follow {
follower_id: user.0.value(),
target: FollowTarget::Handle(body.handle),
@@ -372,12 +348,12 @@ pub async fn unfollow(
user: AuthenticatedUser,
Json(body): Json<ActorUrlRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Unfollow {
follower_id: user.0.value(),
target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url),
target: instance.identify(&body.actor_url),
},
)
.await?;
@@ -398,15 +374,12 @@ pub async fn accept_follower(
user: AuthenticatedUser,
Json(body): Json<ActorUrlRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::AcceptFollow {
owner_id: user.0.value(),
requester: SocialIdentity::from_actor_url(
&body.actor_url,
&state.app_ctx.config.base_url,
),
requester: instance.identify(&body.actor_url),
},
)
.await?;
@@ -427,15 +400,12 @@ pub async fn reject_follower(
user: AuthenticatedUser,
Json(body): Json<ActorUrlRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::RejectFollow {
owner_id: user.0.value(),
requester: SocialIdentity::from_actor_url(
&body.actor_url,
&state.app_ctx.config.base_url,
),
requester: instance.identify(&body.actor_url),
},
)
.await?;
@@ -456,15 +426,12 @@ pub async fn remove_follower(
user: AuthenticatedUser,
Json(body): Json<ActorUrlRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::RemoveFollower {
owner_id: user.0.value(),
follower: SocialIdentity::from_actor_url(
&body.actor_url,
&state.app_ctx.config.base_url,
),
follower: instance.identify(&body.actor_url),
},
)
.await?;
@@ -483,22 +450,101 @@ pub async fn get_pending_followers(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<ActorListResponse>, ApiError> {
let deps = SocialQueryDeps::from(&state);
let identities = application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetPending {
user_id: user.0.value(),
},
let instance = state.app_ctx.instance.clone();
let identities = application::social::get_pending_followers::execute(
&state.app_ctx.deps.social.query,
user.0.value(),
)
.await?;
Ok(Json(ActorListResponse {
actors: identities.into_iter().map(social_actor_to_dto).collect(),
actors: identities
.into_iter()
.map(|a| social_actor_to_dto(a, &instance))
.collect(),
}))
}
#[utoipa::path(
get, path = "/api/v1/social/followers/pending/count",
responses(
(status = 200, body = PendingCountResponse),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn get_pending_follower_count(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<PendingCountResponse>, ApiError> {
let count = application::social::count_pending_followers::execute(
&state.app_ctx.deps.social.query,
user.0.value(),
)
.await?;
Ok(Json(PendingCountResponse { count }))
}
#[utoipa::path(
get, path = "/api/v1/social/following/pending",
responses(
(status = 200, body = ActorListResponse),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn get_pending_following(
State(state): State<AppState>,
user: AuthenticatedUser,
) -> Result<Json<ActorListResponse>, ApiError> {
let instance = state.app_ctx.instance.clone();
let actors = application::social::get_pending_following::execute(
&state.app_ctx.deps.social.query,
user.0.value(),
)
.await?;
Ok(Json(ActorListResponse {
actors: actors
.into_iter()
.map(|a| social_actor_to_dto(a, &instance))
.collect(),
}))
}
#[derive(serde::Deserialize)]
pub struct RelationshipQuery {
pub actor_url: String,
}
#[utoipa::path(
get, path = "/api/v1/social/relationship",
params(("actor_url" = String, Query, description = "Canonical actor URL of the target")),
responses(
(status = 200, body = FollowRelationResponse),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
)]
pub async fn get_relationship(
State(state): State<AppState>,
user: AuthenticatedUser,
Query(q): Query<RelationshipQuery>,
) -> Result<Json<FollowRelationResponse>, ApiError> {
let target = state.app_ctx.instance.identify(&q.actor_url);
let rel = application::social::get_relation::execute(
&state.app_ctx.deps.social.query,
user.0.value(),
target,
)
.await?;
Ok(Json(FollowRelationResponse {
following: rel.following.into(),
followed_by: rel.followed_by.into(),
}))
}
// ── HTML ─────────────────────────────────────────────────────────────────────
pub async fn follow_remote_user(
pub async fn follow_user(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
Path(profile_user_uuid): Path<Uuid>,
@@ -518,9 +564,8 @@ pub async fn follow_remote_user(
.unwrap_or(&format!("/users/{}", profile_user_uuid))
.to_string();
let deps = SocialCommandDeps::from(&state);
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Follow {
follower_id: user_id.value(),
target: FollowTarget::Handle(form.handle),
@@ -542,7 +587,7 @@ pub async fn follow_remote_user(
}
}
pub async fn unfollow_remote_user(
pub async fn unfollow_user(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
Path(profile_user_uuid): Path<Uuid>,
@@ -555,12 +600,12 @@ pub async fn unfollow_remote_user(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Unfollow {
follower_id: user_id.value(),
target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url),
target: instance.identify(&form.actor_url),
},
)
.await
@@ -592,15 +637,12 @@ pub async fn accept_follower_html(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::AcceptFollow {
owner_id: user_id.value(),
requester: SocialIdentity::from_actor_url(
&form.actor_url,
&state.app_ctx.config.base_url,
),
requester: instance.identify(&form.actor_url),
},
)
.await
@@ -626,15 +668,12 @@ pub async fn reject_follower_html(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::RejectFollow {
owner_id: user_id.value(),
requester: SocialIdentity::from_actor_url(
&form.actor_url,
&state.app_ctx.config.base_url,
),
requester: instance.identify(&form.actor_url),
},
)
.await
@@ -647,6 +686,7 @@ pub async fn reject_follower_html(
}
}
#[cfg(feature = "federation")]
pub async fn get_followers_collection(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
@@ -661,8 +701,7 @@ pub async fn get_followers_collection(
let page = params.get("page").and_then(|p| p.parse::<u32>().ok());
return match state
.app_ctx
.services
.ap_service
.ap_document
.followers_collection_json(user_id, page)
.await
{
@@ -674,12 +713,16 @@ pub async fn get_followers_collection(
json,
)
.into_response(),
Err(_) => StatusCode::NOT_FOUND.into_response(),
Err(e) => {
tracing::error!("followers_collection_json error: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
};
}
axum::response::Redirect::to(&format!("/users/{}/followers-list", user_id)).into_response()
}
#[cfg(feature = "federation")]
pub async fn get_following_collection(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
@@ -694,8 +737,7 @@ pub async fn get_following_collection(
let page = params.get("page").and_then(|p| p.parse::<u32>().ok());
return match state
.app_ctx
.services
.ap_service
.ap_document
.following_collection_json(user_id, page)
.await
{
@@ -707,7 +749,10 @@ pub async fn get_following_collection(
json,
)
.into_response(),
Err(_) => StatusCode::NOT_FOUND.into_response(),
Err(e) => {
tracing::error!("following_collection_json error: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
};
}
axum::response::Redirect::to(&format!("/users/{}/following-list", user_id)).into_response()
@@ -729,24 +774,39 @@ pub async fn get_following_page(
"{}/users/{}/following-list",
state.app_ctx.config.base_url, profile_user_uuid
);
let deps = SocialQueryDeps::from(&state);
match application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetFollowing {
user_id: user_id.value(),
},
let instance = state.app_ctx.instance.clone();
match application::social::get_following::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
Ok(following) => {
let actors: Vec<RemoteActorData> = following
.into_iter()
.map(social_actor_to_template)
.map(|a| social_actor_to_template(a, &instance))
.collect();
let pending_actors: Vec<RemoteActorData> =
match application::social::get_pending_following::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
Ok(pending) => pending
.into_iter()
.map(|a| social_actor_to_template(a, &instance))
.collect(),
Err(e) => {
tracing::error!("get_pending_following error: {:?}", e);
Vec::new()
}
};
render_page(FollowingTemplate {
ctx,
user_id: profile_user_uuid,
actors,
pending_actors,
error: params.error,
})
.into_response()
@@ -778,19 +838,17 @@ pub async fn get_followers_page(
"{}/users/{}/followers-list",
state.app_ctx.config.base_url, profile_user_uuid
);
let deps = SocialQueryDeps::from(&state);
match application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetFollowers {
user_id: user_id.value(),
},
let instance = state.app_ctx.instance.clone();
match application::social::get_followers::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
Ok(followers) => {
let actors: Vec<RemoteActorData> = followers
.into_iter()
.map(social_actor_to_template)
.map(|a| social_actor_to_template(a, &instance))
.collect();
render_page(FollowersTemplate {
ctx,
@@ -824,15 +882,12 @@ pub async fn remove_follower_html(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::RemoveFollower {
owner_id: user_id.value(),
follower: SocialIdentity::from_actor_url(
&form.actor_url,
&state.app_ctx.config.base_url,
),
follower: instance.identify(&form.actor_url),
},
)
.await
@@ -851,6 +906,7 @@ pub async fn remove_follower_html(
}
}
#[cfg(feature = "federation")]
pub async fn get_blocked_domains_page(
crate::extractors::AdminUser(user_id): crate::extractors::AdminUser,
State(state): State<AppState>,
@@ -859,13 +915,7 @@ pub async fn get_blocked_domains_page(
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
ctx.page_title = "Blocked Domains — Movies Diary".to_string();
ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url);
match state
.app_ctx
.services
.ap_service
.get_blocked_domains()
.await
{
match state.app_ctx.ap_blocklist.get_blocked_domains().await {
Ok(domains) => {
let entries: Vec<template_askama::BlockedDomainEntry> = domains
.into_iter()
@@ -892,6 +942,7 @@ pub async fn get_blocked_domains_page(
}
}
#[cfg(feature = "federation")]
pub async fn post_blocked_domain(
crate::extractors::AdminUser(_): crate::extractors::AdminUser,
State(state): State<AppState>,
@@ -904,8 +955,7 @@ pub async fn post_blocked_domain(
let reason = form.reason.as_deref().filter(|s| !s.trim().is_empty());
match state
.app_ctx
.services
.ap_service
.ap_blocklist
.add_blocked_domain(&form.domain, reason)
.await
{
@@ -917,6 +967,7 @@ pub async fn post_blocked_domain(
}
}
#[cfg(feature = "federation")]
pub async fn post_remove_blocked_domain(
crate::extractors::AdminUser(_): crate::extractors::AdminUser,
State(state): State<AppState>,
@@ -928,8 +979,7 @@ pub async fn post_remove_blocked_domain(
}
match state
.app_ctx
.services
.ap_service
.ap_blocklist
.remove_blocked_domain(&form.domain)
.await
{
@@ -941,6 +991,7 @@ pub async fn post_remove_blocked_domain(
}
}
#[cfg(feature = "federation")]
pub async fn get_blocked_actors_page(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
@@ -949,12 +1000,10 @@ pub async fn get_blocked_actors_page(
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
ctx.page_title = "Blocked Users — Movies Diary".to_string();
ctx.canonical_url = format!("{}/social/blocked", state.app_ctx.config.base_url);
let deps = SocialQueryDeps::from(&state);
match application::social::execute::execute_query(
&deps,
application::social::queries::SocialQry::GetBlocked {
user_id: user_id.value(),
},
let instance = state.app_ctx.instance.clone();
match application::social::get_blocked::execute(
&state.app_ctx.deps.social.query,
user_id.value(),
)
.await
{
@@ -962,7 +1011,7 @@ pub async fn get_blocked_actors_page(
let entries: Vec<template_askama::BlockedActorEntry> = blocked
.into_iter()
.map(|a| template_askama::BlockedActorEntry {
url: actor_url(&a.identity),
url: instance.actor_url_of(&a.identity),
handle: a.handle,
display_name: a.display_name,
avatar_url: a.avatar_url,
@@ -985,6 +1034,7 @@ pub async fn get_blocked_actors_page(
}
}
#[cfg(feature = "federation")]
pub async fn post_block_actor_html(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
@@ -994,12 +1044,12 @@ pub async fn post_block_actor_html(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Block {
blocker_id: user_id.value(),
target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url),
target: instance.identify(&form.actor_url),
},
)
.await
@@ -1012,6 +1062,7 @@ pub async fn post_block_actor_html(
}
}
#[cfg(feature = "federation")]
pub async fn post_unblock_actor(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
@@ -1021,12 +1072,12 @@ pub async fn post_unblock_actor(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
let deps = SocialCommandDeps::from(&state);
let instance = state.app_ctx.instance.clone();
match application::social::execute::execute_command(
&deps,
&state.app_ctx.deps.social.command,
application::social::commands::SocialCmd::Unblock {
blocker_id: user_id.value(),
target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url),
target: instance.identify(&form.actor_url),
},
)
.await
@@ -1038,3 +1089,110 @@ pub async fn post_unblock_actor(
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId};
fn local_actor(uid: &UserId) -> SocialActor {
SocialActor {
identity: SocialIdentity::Local(uid.clone()),
handle: "@gabriel@md.example".into(),
display_name: None,
avatar_url: None,
}
}
/// The bug: a local actor used to serialize as `local:{uuid}`, which
/// `identify` cannot parse, so every POST echoing this url back resolved
/// to Remote and was routed to ActivityPub.
#[test]
fn local_actor_dto_url_round_trips_back_to_the_same_identity() {
let instance = InstanceIdentity::new("https://md.example");
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
let dto = social_actor_to_dto(local_actor(&uid), &instance);
assert!(!dto.url.starts_with("local:"), "url was {}", dto.url);
assert_eq!(
instance.identify(&dto.url),
SocialIdentity::Local(uid),
"a local actor's dto url must parse back as Local"
);
}
#[cfg(feature = "federation")]
#[test]
fn local_blocked_dto_url_round_trips_back_to_the_same_identity() {
let instance = InstanceIdentity::new("https://md.example");
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
let dto = social_actor_to_blocked_dto(local_actor(&uid), &instance);
assert!(!dto.url.starts_with("local:"), "url was {}", dto.url);
assert_eq!(instance.identify(&dto.url), SocialIdentity::Local(uid));
}
#[test]
fn local_template_url_round_trips_back_to_the_same_identity() {
let instance = InstanceIdentity::new("https://md.example");
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
let data = social_actor_to_template(local_actor(&uid), &instance);
assert!(!data.url.starts_with("local:"), "url was {}", data.url);
assert_eq!(instance.identify(&data.url), SocialIdentity::Local(uid));
}
#[test]
fn remote_actor_dto_url_is_passed_through_unchanged() {
let instance = InstanceIdentity::new("https://md.example");
let actor = SocialActor {
identity: SocialIdentity::Remote {
actor_url: "https://other.example/users/bob".into(),
},
handle: "@bob@other.example".into(),
display_name: None,
avatar_url: None,
};
let dto = social_actor_to_dto(actor, &instance);
assert_eq!(dto.url, "https://other.example/users/bob");
}
#[test]
fn local_actor_dto_carries_user_id_and_avatar() {
let instance = InstanceIdentity::new("https://md.example");
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
let mut actor = local_actor(&uid);
actor.avatar_url = Some("https://md.example/images/a.webp".into());
let dto = social_actor_to_dto(actor, &instance);
assert_eq!(
dto.user_id,
Some(uid.value()),
"local actors must carry user_id for internal links"
);
assert_eq!(
dto.avatar_url.as_deref(),
Some("https://md.example/images/a.webp")
);
}
#[test]
fn remote_actor_dto_has_no_user_id() {
let instance = InstanceIdentity::new("https://md.example");
let actor = SocialActor {
identity: SocialIdentity::Remote {
actor_url: "https://other.example/users/bob".into(),
},
handle: "@bob@other.example".into(),
display_name: None,
avatar_url: None,
};
assert_eq!(social_actor_to_dto(actor, &instance).user_id, None);
}
}

View File

@@ -9,12 +9,12 @@ use axum::{
use uuid::Uuid;
use application::users::{
deps::{GetProfileDeps, UpdateProfileDeps},
get_profile as get_user_profile_uc, get_users,
get_federated_profile, get_federated_profile_stats, get_local_profile,
get_profile_settings as get_profile_settings_uc, get_users,
queries::{GetUserProfileQuery, GetUsersQuery},
update_profile, update_profile_fields,
resolve_username_to_id, update_profile, update_profile_fields,
};
use domain::value_objects::UserId;
use domain::{errors::DomainError, value_objects::Username};
use crate::{
csrf::CsrfToken,
@@ -53,24 +53,20 @@ pub async fn get_profile(
AuthenticatedUser(user_id): AuthenticatedUser,
) -> Result<Json<ProfileResponse>, ApiError> {
let profile = application::users::get_current_profile::execute(
state.app_ctx.repos.user.clone(),
&state.app_ctx.deps.users.get_current_profile,
application::users::queries::GetCurrentProfileQuery {
user_id: user_id.value(),
},
)
.await?;
let base_url = &state.app_ctx.config.base_url;
let instance = &state.app_ctx.instance;
Ok(Json(ProfileResponse {
profile: api_types::UserProfileBase {
username: profile.username,
display_name: profile.display_name,
bio: profile.bio,
avatar_url: profile
.avatar_path
.map(|p| format!("{}/images/{}", base_url, p)),
banner_url: profile
.banner_path
.map(|p| format!("{}/images/{}", base_url, p)),
avatar_url: profile.avatar_path.map(|p| instance.image_url_for(&p)),
banner_url: profile.banner_path.map(|p| instance.image_url_for(&p)),
},
also_known_as: profile.also_known_as,
fields: profile
@@ -113,12 +109,7 @@ pub async fn update_profile_handler(
also_known_as: data.also_known_as,
};
let deps = UpdateProfileDeps {
user: state.app_ctx.repos.user.clone(),
object_storage: state.app_ctx.services.object_storage.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match update_profile::execute(&deps, cmd).await {
match update_profile::execute(&state.app_ctx.deps.users.update_profile, cmd).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => crate::errors::domain_error_response(e),
}
@@ -159,12 +150,7 @@ pub async fn update_profile_fields_handler(
fields,
};
match update_profile_fields::execute(
state.app_ctx.repos.profile_fields.clone(),
state.app_ctx.services.event_publisher.clone(),
cmd,
)
.await
match update_profile_fields::execute(&state.app_ctx.deps.users.update_profile_fields, cmd).await
{
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => crate::errors::domain_error_response(e),
@@ -176,11 +162,8 @@ pub async fn update_profile_fields_handler(
responses((status = 200, body = UsersResponse)),
)]
pub async fn list_users(State(state): State<AppState>) -> Result<Json<UsersResponse>, ApiError> {
let deps = application::users::deps::GetUsersListDeps {
user: state.app_ctx.repos.user.clone(),
federation_admin: state.app_ctx.repos.federation_admin.clone(),
};
let result = get_users::execute(&deps, GetUsersQuery).await?;
let result =
get_users::execute(&state.app_ctx.deps.users.get_users_list, GetUsersQuery).await?;
Ok(Json(UsersResponse {
users: result
.users
@@ -220,44 +203,15 @@ pub async fn get_user_profile(
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
};
let local_user = match state
.app_ctx
.repos
.user
.find_by_id(&UserId::from_uuid(user_id))
.await
{
Ok(u) => u,
Err(e) => {
return crate::errors::domain_error_response(e);
}
};
if local_user.is_none() {
if let Some(ref fed_query) = state.app_ctx.repos.federated_profile
&& let Ok(Some(fed)) = fed_query.get_federated_profile(user_id).await
{
return build_federated_profile_response(&state, user_id, fed, profile_view, &params)
.await;
}
return StatusCode::NOT_FOUND.into_response();
}
let user = local_user.unwrap();
let get_profile_deps = GetProfileDeps {
stats: state.app_ctx.repos.stats.clone(),
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query_unified.clone(),
};
let profile = match get_user_profile_uc::execute(
&get_profile_deps,
let profile = match get_local_profile::execute(
&state.app_ctx.deps.users.get_local_profile,
GetUserProfileQuery {
user_id,
view: profile_view,
limit: params.limit,
offset: params.offset,
sort_by: domain::models::FeedSortBy::Date,
search: params.search,
search: params.search.clone(),
is_own_profile: viewer_id.value() == user_id,
include_remote: false,
},
@@ -265,6 +219,24 @@ pub async fn get_user_profile(
.await
{
Ok(p) => p,
Err(DomainError::NotFound(_)) => {
if let Ok(Some(fed)) = get_federated_profile::execute(
&state.app_ctx.deps.users.get_federated_profile,
user_id,
)
.await
{
return build_federated_profile_response(
&state,
user_id,
fed,
profile_view,
&params,
)
.await;
}
return StatusCode::NOT_FOUND.into_response();
}
Err(e) => return crate::errors::domain_error_response(e),
};
@@ -306,15 +278,11 @@ pub async fn get_user_profile(
Json(UserProfileResponse {
user_id,
profile: api_types::UserProfileBase {
username: user.username().value().to_string(),
avatar_url: user
.avatar_path()
.map(|p| format!("{}/images/{}", state.app_ctx.config.base_url, p)),
banner_url: user
.banner_path()
.map(|p| format!("{}/images/{}", state.app_ctx.config.base_url, p)),
display_name: None,
bio: None,
username: profile.identity.username.clone(),
avatar_url: profile.identity.avatar_url.clone(),
banner_url: profile.identity.banner_url.clone(),
display_name: profile.identity.display_name.clone(),
bio: profile.identity.bio.clone(),
},
stats: UserStatsDto {
total_movies: profile.stats.total_movies,
@@ -327,10 +295,7 @@ pub async fn get_user_profile(
view_data,
goals: {
let goals_list = application::goals::list::execute(
&application::goals::deps::GoalQueryDeps {
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
},
&state.app_ctx.deps.goals.query,
application::goals::queries::ListGoalsQuery { user_id },
)
.await
@@ -342,8 +307,8 @@ pub async fn get_user_profile(
}
},
is_federated: false,
handle: None,
actor_url: None,
handle: Some(profile.identity.handle.clone()),
actor_url: Some(profile.identity.actor_url.clone()),
})
.into_response()
}
@@ -355,13 +320,8 @@ async fn build_federated_profile_response(
profile_view: application::users::queries::ProfileView,
params: &UserProfileQueryParams,
) -> axum::response::Response {
let get_profile_deps = GetProfileDeps {
stats: state.app_ctx.repos.stats.clone(),
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query_unified.clone(),
};
let profile = match get_user_profile_uc::execute(
&get_profile_deps,
let profile = match get_federated_profile_stats::execute(
&state.app_ctx.deps.users.get_federated_profile_stats,
GetUserProfileQuery {
user_id,
view: profile_view,
@@ -481,12 +441,8 @@ pub async fn get_users_list(
ctx.page_title = "Members — Movies Diary".to_string();
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
let users_deps = application::users::deps::GetUsersListDeps {
user: state.app_ctx.repos.user.clone(),
federation_admin: state.app_ctx.repos.federation_admin.clone(),
};
match application::users::get_users::execute(
&users_deps,
&state.app_ctx.deps.users.get_users_list,
application::users::queries::GetUsersQuery,
)
.await
@@ -517,14 +473,14 @@ pub async fn get_user_by_username(
State(state): State<AppState>,
Path(username): Path<String>,
) -> impl IntoResponse {
let uname = match domain::value_objects::Username::new(username) {
let uname = match Username::new(username) {
Ok(u) => u,
Err(_) => return StatusCode::NOT_FOUND.into_response(),
};
match state.app_ctx.repos.user.find_by_username(&uname).await {
Ok(Some(user)) => {
axum::response::Redirect::permanent(&format!("/users/{}", user.id().value()))
.into_response()
match resolve_username_to_id::execute(&state.app_ctx.deps.users.resolve_username, &uname).await
{
Ok(Some(uid)) => {
axum::response::Redirect::permanent(&format!("/users/{}", uid.value())).into_response()
}
_ => StatusCode::NOT_FOUND.into_response(),
}
@@ -607,10 +563,7 @@ async fn fetch_profile_goals(
user_id: Uuid,
) -> Vec<template_askama::GoalViewData> {
let goals_list = application::goals::list::execute(
&application::goals::deps::GoalQueryDeps {
goal_query: state.app_ctx.repos.goal_query.clone(),
stats: state.app_ctx.repos.stats.clone(),
},
&state.app_ctx.deps.goals.query,
application::goals::queries::ListGoalsQuery { user_id },
)
.await
@@ -647,8 +600,7 @@ pub async fn get_user_profile_html(
if accept.contains("application/activity+json") || accept.contains("application/ld+json") {
return match state
.app_ctx
.services
.ap_service
.ap_document
.actor_json(&profile_user_uuid.to_string())
.await
{
@@ -681,25 +633,6 @@ pub async fn get_user_profile_html(
}
};
let profile_user = match state
.app_ctx
.repos
.user
.find_by_id(&domain::value_objects::UserId::from_uuid(profile_user_uuid))
.await
{
Ok(Some(u)) => u,
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
Err(e) => return crate::errors::domain_error_response(e),
};
let display_name = profile_user.username().value();
ctx.page_title = format!("{}'s Diary — Movies Diary", display_name);
ctx.canonical_url = format!(
"{}/users/{}",
state.app_ctx.config.base_url, profile_user_uuid
);
let sort_by_str = match params.sort_by.as_str() {
"date_asc" => "date_asc",
"rating" => "rating",
@@ -727,19 +660,25 @@ pub async fn get_user_profile_html(
include_remote: false,
};
let html_profile_deps = GetProfileDeps {
stats: state.app_ctx.repos.stats.clone(),
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query_unified.clone(),
};
match application::users::get_profile::execute(&html_profile_deps, query).await {
match get_local_profile::execute(&state.app_ctx.deps.users.get_local_profile, query).await {
Ok(profile) => {
ctx.page_title = format!("{}'s Diary — Movies Diary", profile.identity.username);
ctx.canonical_url = format!(
"{}/users/{}",
state.app_ctx.config.base_url, profile_user_uuid
);
let pag = compute_pagination(profile.entries.as_ref());
if !is_own_profile {
ctx.page_rss_url = Some(format!("/users/{}/feed.rss", profile_user_uuid));
}
let email = profile_user.email().value().to_string();
let display_name = email.split('@').next().unwrap_or("?").to_string();
let display_name = profile
.identity
.email
.split('@')
.next()
.unwrap_or("?")
.to_string();
let stats_disp = build_stats_display(&profile.stats);
let history = profile.history.map(application::users::group_by_month);
let heatmap = history.as_deref().map(build_heatmap).unwrap_or_default();
@@ -809,6 +748,7 @@ pub async fn get_user_profile_html(
.into_response()
}
}
Err(DomainError::NotFound(_)) => StatusCode::NOT_FOUND.into_response(),
Err(e) => crate::errors::domain_error_response(e),
}
}
@@ -828,43 +768,26 @@ pub async fn get_profile_settings(
ctx.page_title = "Profile Settings — Movies Diary".to_string();
ctx.canonical_url = format!("{}/settings/profile", state.app_ctx.config.base_url);
let user = match state.app_ctx.repos.user.find_by_id(&user_id).await {
Ok(Some(u)) => u,
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
let settings = match get_profile_settings_uc::execute(
&state.app_ctx.deps.users.get_profile_settings,
user_id.value(),
)
.await
{
Ok(s) => s,
Err(DomainError::NotFound(_)) => return StatusCode::NOT_FOUND.into_response(),
Err(e) => return crate::errors::domain_error_response(e),
};
let base_url = &state.app_ctx.config.base_url;
let avatar_url = user
.avatar_path()
.map(|path| format!("{}/images/{}", base_url, path));
let banner_url = user
.banner_path()
.map(|path| format!("{}/images/{}", base_url, path));
let profile_fields: Vec<(String, String)> = state
.app_ctx
.repos
.profile_fields
.get_fields(&user_id)
.await
.unwrap_or_default()
.into_iter()
.map(|f| (f.name, f.value))
.collect();
let saved = params.saved.as_deref() == Some("1");
let bio = user.bio().map(|s| s.to_string());
let also_known_as = user.also_known_as().map(|s| s.to_string());
render_page(ProfileSettingsTemplate {
ctx: &ctx,
bio: bio.as_deref(),
avatar_url: avatar_url.as_deref(),
banner_url: banner_url.as_deref(),
also_known_as: also_known_as.as_deref(),
profile_fields: &profile_fields,
bio: settings.bio.as_deref(),
avatar_url: settings.avatar_url.as_deref(),
banner_url: settings.banner_url.as_deref(),
also_known_as: settings.also_known_as.as_deref(),
profile_fields: &settings.fields,
saved,
embed_url: format!(
"{}/users/{}?embed=true",
@@ -892,12 +815,7 @@ pub async fn post_profile_settings(
banner_content_type: data.banner_content_type,
also_known_as: data.also_known_as,
};
let update_deps = UpdateProfileDeps {
user: state.app_ctx.repos.user.clone(),
object_storage: state.app_ctx.services.object_storage.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
if let Err(e) = update_profile::execute(&update_deps, cmd).await {
if let Err(e) = update_profile::execute(&state.app_ctx.deps.users.update_profile, cmd).await {
tracing::error!("update_profile error: {:?}", e);
return axum::response::Redirect::to(&format!(
"/settings/profile?error={}",
@@ -925,12 +843,9 @@ pub async fn post_profile_settings(
user_id: user_id.value(),
fields,
};
if let Err(e) = update_profile_fields::execute(
state.app_ctx.repos.profile_fields.clone(),
state.app_ctx.services.event_publisher.clone(),
fields_cmd,
)
.await
if let Err(e) =
update_profile_fields::execute(&state.app_ctx.deps.users.update_profile_fields, fields_cmd)
.await
{
tracing::error!("update_profile_fields error: {:?}", e);
return axum::response::Redirect::to(&format!(

View File

@@ -11,8 +11,7 @@ use application::{
watchlist::{
add as add_to_watchlist,
commands::{AddToWatchlistCommand, RemoveFromWatchlistCommand},
deps::WatchlistAddDeps,
get as get_watchlist, is_on as is_on_watchlist,
get as get_watchlist, get_watchlist_for_owner, is_on as is_on_watchlist,
queries::{GetWatchlistQuery, IsOnWatchlistQuery},
remove as remove_from_watchlist,
},
@@ -54,7 +53,7 @@ pub async fn get_watchlist_handler(
Query(params): Query<PaginationQueryParams>,
) -> Result<Json<WatchlistResponse>, ApiError> {
let page = get_watchlist::execute(
state.app_ctx.repos.watchlist.clone(),
&state.app_ctx.deps.watchlist.get_watchlist,
GetWatchlistQuery {
user_id: user.0.value(),
limit: params.limit,
@@ -94,15 +93,8 @@ pub async fn post_watchlist_add(
user: AuthenticatedUser,
Json(req): Json<AddToWatchlistRequest>,
) -> Result<impl IntoResponse, ApiError> {
let deps = WatchlistAddDeps {
movie_command: state.app_ctx.repos.movie_command.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
metadata: state.app_ctx.services.metadata.clone(),
watchlist: state.app_ctx.repos.watchlist.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
add_to_watchlist::execute(
&deps,
&state.app_ctx.deps.watchlist.add,
AddToWatchlistCommand {
user_id: user.0.value(),
input: MovieInput {
@@ -134,8 +126,7 @@ pub async fn delete_watchlist_entry(
Path(movie_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
remove_from_watchlist::execute(
state.app_ctx.repos.watchlist.clone(),
state.app_ctx.services.event_publisher.clone(),
&state.app_ctx.deps.watchlist.remove_from_watchlist,
RemoveFromWatchlistCommand {
user_id: user.0.value(),
movie_id,
@@ -160,7 +151,7 @@ pub async fn get_watchlist_status(
Path(movie_id): Path<Uuid>,
) -> Result<Json<WatchlistStatusResponse>, ApiError> {
let on_watchlist = is_on_watchlist::execute(
state.app_ctx.repos.watchlist.clone(),
&state.app_ctx.deps.watchlist.is_on_watchlist,
IsOnWatchlistQuery {
user_id: user.0.value(),
movie_id,
@@ -182,39 +173,25 @@ pub async fn get_watchlist_page(
let ctx = build_page_context(&state, viewer_id.clone(), csrf.0).await;
let is_owner = viewer_id.map(|u| u.value() == owner_id).unwrap_or(false);
let user_id = domain::value_objects::UserId::from_uuid(owner_id);
let is_local = state
.app_ctx
.repos
.user
.find_by_id(&user_id)
.await
.map(|u| u.is_some())
.unwrap_or(false);
let view = match get_watchlist_for_owner::execute(
&state.app_ctx.deps.watchlist.get_watchlist_for_owner,
owner_id,
params.limit.or(Some(20)),
params.offset.or(Some(0)),
)
.await
{
Ok(view) => view,
Err(e) => return crate::errors::domain_error_response(e),
};
let result = if is_local {
match get_watchlist::execute(
state.app_ctx.repos.watchlist.clone(),
application::watchlist::queries::GetWatchlistQuery {
user_id: owner_id,
limit: params.limit.or(Some(20)),
offset: params.offset.or(Some(0)),
},
)
.await
{
Ok(page) => crate::mappers::watchlist::build_watchlist_page(page, is_owner),
Err(e) => return crate::errors::domain_error_response(e),
let result = match view {
get_watchlist_for_owner::WatchlistView::Local(page) => {
crate::mappers::watchlist::build_watchlist_page(page, is_owner)
}
get_watchlist_for_owner::WatchlistView::Remote(entries) => {
crate::mappers::watchlist::build_remote_watchlist_page(entries)
}
} else {
let remote_entries = state
.app_ctx
.repos
.remote_watchlist
.get_by_derived_uuid(owner_id)
.await
.unwrap_or_default();
crate::mappers::watchlist::build_remote_watchlist_page(remote_entries)
};
render_page(WatchlistTemplate {
@@ -280,16 +257,8 @@ pub async fn post_watchlist_add_html(
}
};
let deps = WatchlistAddDeps {
movie_command: state.app_ctx.repos.movie_command.clone(),
movie_query: state.app_ctx.repos.movie_query.clone(),
metadata: state.app_ctx.services.metadata.clone(),
watchlist: state.app_ctx.repos.watchlist.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match add_to_watchlist::execute(
&deps,
&state.app_ctx.deps.watchlist.add,
AddToWatchlistCommand {
user_id: user_id.value(),
input,
@@ -323,8 +292,7 @@ pub async fn post_watchlist_remove_html(
return StatusCode::FORBIDDEN.into_response();
}
match remove_from_watchlist::execute(
state.app_ctx.repos.watchlist.clone(),
state.app_ctx.services.event_publisher.clone(),
&state.app_ctx.deps.watchlist.remove_from_watchlist,
RemoveFromWatchlistCommand {
user_id: user_id.value(),
movie_id,

View File

@@ -15,10 +15,9 @@ use application::integrations::{
ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand,
IngestWatchEventCommand, RevokeWebhookTokenCommand, WatchEventConfirmation,
},
confirm as confirm_watch_events,
deps::IngestWatchEventDeps,
dismiss as dismiss_watch_events, generate_token as generate_webhook_token,
get_queue as get_watch_queue, get_tokens as get_webhook_tokens, ingest as ingest_watch_event,
confirm as confirm_watch_events, dismiss as dismiss_watch_events,
generate_token as generate_webhook_token, get_queue as get_watch_queue,
get_tokens as get_webhook_tokens, ingest as ingest_watch_event,
queries::{GetWatchQueueQuery, GetWebhookTokensQuery},
revoke_token as revoke_webhook_token,
};
@@ -72,7 +71,12 @@ pub async fn post_jellyfin_webhook(
source: WatchEventSource::Jellyfin,
};
run_ingest(&state, cmd, &jellyfin::JellyfinParser).await
run_ingest(
&state,
cmd,
&*state.app_ctx.deps.integrations.jellyfin_parser,
)
.await
}
// ── Plex webhook (multipart form data with `payload` JSON field) ──────────────
@@ -119,7 +123,7 @@ pub async fn post_plex_webhook(
source: WatchEventSource::Plex,
};
run_ingest(&state, cmd, &plex::PlexParser).await
run_ingest(&state, cmd, &*state.app_ctx.deps.integrations.plex_parser).await
}
async fn run_ingest(
@@ -127,13 +131,13 @@ async fn run_ingest(
cmd: IngestWatchEventCommand,
parser: &dyn domain::ports::MediaServerParser,
) -> StatusCode {
let deps = IngestWatchEventDeps {
webhook_token: state.app_ctx.repos.webhook_token.clone(),
watch_event_command: state.app_ctx.repos.watch_event_command.clone(),
watch_event_query: state.app_ctx.repos.watch_event_query.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match ingest_watch_event::execute(&deps, cmd, parser).await {
match ingest_watch_event::execute(
&state.app_ctx.deps.integrations.ingest_watch_event,
cmd,
parser,
)
.await
{
Ok(()) => StatusCode::OK,
Err(e) => crate::errors::domain_error_status(&e),
}
@@ -166,8 +170,11 @@ pub async fn post_generate_webhook_token(
label: req.label,
};
let result =
generate_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await?;
let result = generate_webhook_token::execute(
&state.app_ctx.deps.integrations.generate_webhook_token,
cmd,
)
.await?;
let base_url = &state.app_ctx.config.base_url;
let webhook_url = format!("{base_url}/api/v1/webhooks/{provider}");
@@ -195,7 +202,8 @@ pub async fn get_webhook_tokens(
user_id: user.0.value(),
};
let tokens =
get_webhook_tokens::execute(state.app_ctx.repos.webhook_token.clone(), query).await?;
get_webhook_tokens::execute(&state.app_ctx.deps.integrations.get_webhook_tokens, query)
.await?;
let dtos = tokens
.into_iter()
@@ -230,7 +238,8 @@ pub async fn delete_webhook_token(
user_id: user.0.value(),
token_id: id,
};
revoke_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await?;
revoke_webhook_token::execute(&state.app_ctx.deps.integrations.revoke_webhook_token, cmd)
.await?;
Ok(StatusCode::NO_CONTENT)
}
@@ -252,7 +261,7 @@ pub async fn get_watch_queue(
user_id: user.0.value(),
};
let events =
get_watch_queue::execute(state.app_ctx.repos.watch_event_query.clone(), query).await?;
get_watch_queue::execute(&state.app_ctx.deps.integrations.get_watch_queue, query).await?;
let dtos = events
.into_iter()
@@ -297,13 +306,9 @@ pub async fn post_confirm_watch_events(
.collect(),
};
let confirmed = confirm_watch_events::execute(
state.app_ctx.repos.watch_event_command.clone(),
state.app_ctx.repos.watch_event_query.clone(),
state.app_ctx.services.review_logger.clone(),
cmd,
)
.await?;
let confirmed =
confirm_watch_events::execute(&state.app_ctx.deps.integrations.confirm_watch_events, cmd)
.await?;
Ok(Json(ConfirmWatchResponse { confirmed }))
}
@@ -327,11 +332,8 @@ pub async fn post_dismiss_watch_events(
event_ids: req.event_ids,
};
let dismissed = dismiss_watch_events::execute(
state.app_ctx.repos.watch_event_command.clone(),
state.app_ctx.repos.watch_event_query.clone(),
cmd,
)
.await?;
let dismissed =
dismiss_watch_events::execute(&state.app_ctx.deps.integrations.dismiss_watch_events, cmd)
.await?;
Ok(Json(DismissWatchResponse { dismissed }))
}

View File

@@ -9,11 +9,11 @@ use uuid::Uuid;
use application::wrapup::{
commands::RequestWrapUpCommand,
delete as delete_wrapup, generate, get_wrapup,
delete as delete_wrapup, generate, get_ready_report, get_wrapup,
list_wrapups::{self, ListWrapUpsQuery},
};
use domain::errors::DomainError;
use domain::models::wrapup::{WrapUpRecord, WrapUpReport, WrapUpStatus};
use domain::models::wrapup::{WrapUpRecord, WrapUpReport, WrapUpScope, WrapUpStatus};
use domain::value_objects::WrapUpId;
use crate::{
@@ -66,12 +66,7 @@ pub async fn post_generate(
start_date: start,
end_date: end,
};
let id = generate::execute(
state.app_ctx.repos.wrapup_repo.clone(),
state.app_ctx.services.event_publisher.clone(),
cmd,
)
.await?;
let id = generate::execute(&state.app_ctx.deps.wrapup.generate, cmd).await?;
Ok(Json(WrapUpGeneratedResponse {
id: id.value().to_string(),
}))
@@ -90,7 +85,7 @@ pub async fn get_list(
user: AuthenticatedUser,
) -> Result<Json<WrapUpListResponse>, ApiError> {
let records = list_wrapups::execute(
state.app_ctx.repos.wrapup_repo.clone(),
&state.app_ctx.deps.wrapup.list_wrapups,
ListWrapUpsQuery {
user_id: Some(user.0.value()),
},
@@ -117,7 +112,7 @@ pub async fn get_status(
Path(id): Path<Uuid>,
) -> Result<Json<WrapUpStatusResponse>, ApiError> {
let record = get_wrapup::execute(
state.app_ctx.repos.wrapup_repo.clone(),
&state.app_ctx.deps.wrapup.get_wrapup,
WrapUpId::from_uuid(id),
)
.await?
@@ -142,7 +137,7 @@ pub async fn get_report(
Path(id): Path<Uuid>,
) -> impl IntoResponse {
match get_wrapup::execute(
state.app_ctx.repos.wrapup_repo.clone(),
&state.app_ctx.deps.wrapup.get_wrapup,
WrapUpId::from_uuid(id),
)
.await
@@ -177,7 +172,7 @@ pub async fn delete_wrapup_handler(
Path(id): Path<Uuid>,
) -> Result<StatusCode, ApiError> {
delete_wrapup::execute(
state.app_ctx.repos.wrapup_repo.clone(),
&state.app_ctx.deps.wrapup.delete_wrapup,
WrapUpId::from_uuid(id),
)
.await?;
@@ -243,30 +238,15 @@ pub async fn get_user_wrapup_html(
Path((user_id, year)): Path<(Uuid, i32)>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
let start = match NaiveDate::from_ymd_opt(year, 1, 1) {
Some(d) => d,
None => return StatusCode::BAD_REQUEST.into_response(),
};
let end = match NaiveDate::from_ymd_opt(year + 1, 1, 1) {
Some(d) => d,
None => return StatusCode::BAD_REQUEST.into_response(),
};
let record = match state
.app_ctx
.repos
.wrapup_repo
.find_existing(Some(user_id), start, end)
.await
{
Ok(Some(r)) if r.status == WrapUpStatus::Ready => r,
_ => return StatusCode::NOT_FOUND.into_response(),
};
let report = match record.report {
Some(r) => r,
None => return StatusCode::NOT_FOUND.into_response(),
};
let scope = WrapUpScope::User(user_id);
let report =
match get_ready_report::execute(&state.app_ctx.deps.wrapup.get_ready_report, scope, year)
.await
{
Ok(report) => report,
Err(DomainError::ValidationError(_)) => return StatusCode::BAD_REQUEST.into_response(),
Err(_) => return StatusCode::NOT_FOUND.into_response(),
};
let ctx = super::helpers::build_page_context(&state, viewer, csrf.0).await;
render_wrapup(&report, year, &ctx)
@@ -278,29 +258,16 @@ pub async fn get_global_wrapup_html(
Path(year): Path<i32>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
let start = match NaiveDate::from_ymd_opt(year, 1, 1) {
Some(d) => d,
None => return StatusCode::BAD_REQUEST.into_response(),
};
let end = match NaiveDate::from_ymd_opt(year + 1, 1, 1) {
Some(d) => d,
None => return StatusCode::BAD_REQUEST.into_response(),
};
let record = match state
.app_ctx
.repos
.wrapup_repo
.find_existing(None, start, end)
.await
let report = match get_ready_report::execute(
&state.app_ctx.deps.wrapup.get_ready_report,
WrapUpScope::Global,
year,
)
.await
{
Ok(Some(r)) if r.status == WrapUpStatus::Ready => r,
_ => return StatusCode::NOT_FOUND.into_response(),
};
let report = match record.report {
Some(r) => r,
None => return StatusCode::NOT_FOUND.into_response(),
Ok(report) => report,
Err(DomainError::ValidationError(_)) => return StatusCode::BAD_REQUEST.into_response(),
Err(_) => return StatusCode::NOT_FOUND.into_response(),
};
let ctx = super::helpers::build_page_context(&state, viewer, csrf.0).await;