refactor: group use cases into DDD bounded contexts

Flat use_cases/ (44 files) + monolithic commands.rs/queries.rs
split into diary/, movies/, watchlist/, import/, auth/, users/,
integrations/, search/, person/, federation/ — each with own
commands.rs, queries.rs, and use case modules.

Inline tests extracted to sibling tests/ dirs.
This commit is contained in:
2026-06-02 19:49:09 +02:00
parent aadad3cfb0
commit dcc9244d4e
92 changed files with 1617 additions and 1500 deletions

View File

@@ -2,7 +2,7 @@ use chrono::NaiveDateTime;
use serde::Deserialize;
use uuid::Uuid;
use application::{
use application::diary::{
commands::{LogReviewCommand, MovieInput},
queries::GetDiaryQuery,
};

View File

@@ -9,22 +9,31 @@ use uuid::Uuid;
use std::str::FromStr;
use application::{
commands::{
AddToWatchlistCommand, DeleteReviewCommand, MovieInput, RegisterCommand,
RemoveFromWatchlistCommand, SyncPosterCommand,
auth::{
commands::RegisterCommand, login as login_uc, queries::LoginQuery, register as register_uc,
},
queries::{
ExportQuery, GetActivityFeedQuery, GetMovieSocialPageQuery, GetMoviesQuery,
GetReviewHistoryQuery, GetUserProfileQuery, GetUsersQuery, GetWatchlistQuery,
IsOnWatchlistQuery, LoginQuery,
diary::{
commands::{DeleteReviewCommand, MovieInput, SyncPosterCommand},
delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc,
get_diary, get_movie_social_page, get_review_history, log_review,
queries::{
ExportQuery, GetActivityFeedQuery, GetMovieSocialPageQuery, GetReviewHistoryQuery,
},
},
use_cases::{
add_to_watchlist, delete_review, export_diary as export_diary_uc,
get_activity_feed as get_feed_uc, get_diary, get_movie_social_page, get_movies, get_person,
get_person_credits, get_review_history, get_user_profile as get_user_profile_uc, get_users,
get_watchlist, is_on_watchlist, log_review, login as login_uc, register as register_uc,
remove_from_watchlist, search as search_uc, sync_poster, update_profile,
update_profile_fields,
movies::{get_movies, queries::GetMoviesQuery, sync_poster},
person::{get as get_person, get_credits as get_person_credits},
search::execute as search_uc,
users::{
get_profile as get_user_profile_uc, get_users,
queries::{GetUserProfileQuery, GetUsersQuery},
update_profile, update_profile_fields,
},
watchlist::{
add as add_to_watchlist,
commands::{AddToWatchlistCommand, RemoveFromWatchlistCommand},
get as get_watchlist, is_on as is_on_watchlist,
queries::{GetWatchlistQuery, IsOnWatchlistQuery},
remove as remove_from_watchlist,
},
};
use domain::{
@@ -333,12 +342,7 @@ pub async fn get_movie_profile(
Path(movie_id): Path<Uuid>,
) -> impl IntoResponse {
let id = domain::value_objects::MovieId::from_uuid(movie_id);
match state
.app_ctx
.movie_profile_repository
.get_by_movie_id(&id)
.await
{
match state.app_ctx.repos.movie_profile.get_by_movie_id(&id).await {
Ok(Some(p)) => Json(MovieProfileResponse {
tmdb_id: p.tmdb_id,
imdb_id: p.imdb_id,
@@ -413,9 +417,9 @@ pub async fn get_profile(
State(state): State<AppState>,
AuthenticatedUser(user_id): AuthenticatedUser,
) -> impl IntoResponse {
match application::use_cases::get_current_profile::execute(
match application::users::get_current_profile::execute(
&state.app_ctx,
application::queries::GetCurrentProfileQuery {
application::users::queries::GetCurrentProfileQuery {
user_id: user_id.value(),
},
)
@@ -498,7 +502,7 @@ pub async fn update_profile_handler(
}
}
let cmd = application::commands::UpdateProfileCommand {
let cmd = application::users::commands::UpdateProfileCommand {
user_id: user_id.value(),
display_name,
bio,
@@ -552,7 +556,7 @@ pub async fn update_profile_fields_handler(
})
.collect();
let cmd = application::commands::UpdateProfileFieldsCommand {
let cmd = application::users::commands::UpdateProfileFieldsCommand {
user_id: user_id.value(),
fields,
};
@@ -1066,14 +1070,15 @@ pub async fn get_user_profile(
Query(params): Query<UserProfileQueryParams>,
) -> impl IntoResponse {
let view_str = params.view.as_deref().unwrap_or("recent");
let profile_view = match application::queries::ProfileView::from_str(view_str) {
let profile_view = match application::users::queries::ProfileView::from_str(view_str) {
Ok(v) => v,
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
};
let user = match state
.app_ctx
.user_repository
.repos
.user
.find_by_id(&UserId::from_uuid(user_id))
.await
{

View File

@@ -4,40 +4,53 @@ use axum::{
Form,
extract::{Extension, Multipart, Path, Query, State},
http::{HeaderValue, StatusCode, header::SET_COOKIE},
response::{Html, IntoResponse, Redirect},
response::{IntoResponse, Redirect},
};
use chrono::Utc;
use uuid::Uuid;
#[cfg(feature = "federation")]
use application::ports::{
BlockedActorEntry, BlockedActorsPageData, BlockedDomainEntry, BlockedDomainsPageData,
FollowersPageData, FollowingPageData,
};
use application::{
commands::{
AddToWatchlistCommand, ConfirmWatchEventsCommand, DeleteReviewCommand,
DismissWatchEventsCommand, GenerateWebhookTokenCommand, MovieInput,
RemoveFromWatchlistCommand, RevokeWebhookTokenCommand, WatchEventConfirmation,
auth::{login as login_uc, queries::LoginQuery},
diary::{
commands::{DeleteReviewCommand, MovieInput},
delete_review, export_diary as export_diary_uc, get_movie_social_page, log_review,
queries::{ExportQuery, GetMovieSocialPageQuery},
},
ports::{
HtmlPageContext, IntegrationsPageData, LoginPageData, MovieDetailPageData,
NewReviewPageData, ProfileSettingsPageData, RegisterPageData, RemoteActorView,
WatchQueueDisplayEntry, WatchQueuePageData, WatchlistPageData, WebhookTokenView,
integrations::{
commands::{
ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand,
RevokeWebhookTokenCommand, WatchEventConfirmation,
},
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,
queries::{GetWatchQueueQuery, GetWebhookTokensQuery},
revoke_token as revoke_webhook_token,
},
queries::{
ExportQuery, GetMovieSocialPageQuery, GetWatchQueueQuery, GetWebhookTokensQuery,
IsOnWatchlistQuery, LoginQuery,
},
use_cases::{
add_to_watchlist, confirm_watch_events, delete_review, dismiss_watch_events,
export_diary as export_diary_uc, generate_webhook_token, get_movie_social_page,
get_watch_queue, get_webhook_tokens, is_on_watchlist, log_review, login as login_uc,
remove_from_watchlist, revoke_webhook_token, update_profile, update_profile_fields,
users::{update_profile, update_profile_fields},
watchlist::{
add as add_to_watchlist,
commands::{AddToWatchlistCommand, RemoveFromWatchlistCommand},
is_on as is_on_watchlist,
queries::IsOnWatchlistQuery,
remove as remove_from_watchlist,
},
};
use crate::render::render_page;
use application::ports::HtmlPageContext;
use domain::models::ExportFormat;
use domain::{errors::DomainError, value_objects::UserId};
use template_askama::{
ActivityFeedTemplate, IntegrationsTemplate, LoginTemplate, MonthlyRatingRow,
MovieDetailTemplate, NewReviewTemplate, ProfileSettingsTemplate, ProfileTemplate,
RegisterTemplate, RemoteActorData, RemoteActorDisplay, UserSummaryView, UsersTemplate,
WatchQueueTemplate, WatchlistTemplate, bar_height_px, build_heatmap, build_page_items,
};
#[cfg(feature = "federation")]
use template_askama::{
BlockedActorsTemplate, BlockedDomainsTemplate, FollowersTemplate, FollowingTemplate,
};
#[cfg(feature = "federation")]
use crate::forms::{
@@ -57,13 +70,7 @@ pub(crate) async fn build_page_context(
) -> 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
.user_repository
.find_by_id(id)
.await
.ok()
.flatten();
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()
@@ -128,14 +135,10 @@ pub async fn get_login_page(
csrf_token: csrf.0,
page_rss_url: None,
};
let html = state
.html_renderer
.render_login_page(LoginPageData {
ctx,
error: params.error.as_deref(),
})
.expect("login template failed");
Html(html)
render_page(LoginTemplate {
ctx: &ctx,
error: params.error.as_deref(),
})
}
pub async fn post_login(
@@ -195,14 +198,11 @@ pub async fn get_register_page(
csrf_token: csrf.0,
page_rss_url: None,
};
let html = state
.html_renderer
.render_register_page(RegisterPageData {
ctx,
error: params.error.as_deref(),
})
.expect("register template failed");
Html(html).into_response()
render_page(RegisterTemplate {
ctx: &ctx,
error: params.error.as_deref(),
})
.into_response()
}
pub async fn post_register(
@@ -216,9 +216,9 @@ pub async fn post_register(
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
}
match application::use_cases::register_and_login::execute(
match application::auth::register_and_login::execute(
&state.app_ctx,
application::commands::RegisterAndLoginCommand {
application::auth::commands::RegisterAndLoginCommand {
email: form.email,
username: form.username,
password: form.password,
@@ -246,14 +246,10 @@ pub async fn get_new_review_page(
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
ctx.page_title = "Log a Review — Movies Diary".to_string();
ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
let html = state
.html_renderer
.render_new_review_page(NewReviewPageData {
ctx,
error: params.error.as_deref(),
})
.expect("new_review template failed");
Html(html)
render_page(NewReviewTemplate {
ctx: &ctx,
error: params.error.as_deref(),
})
}
pub async fn post_review(
@@ -374,7 +370,7 @@ pub async fn get_activity_feed(
_ => "date",
};
let query = application::queries::GetActivityFeedQuery {
let query = application::diary::queries::GetActivityFeedQuery {
limit,
offset,
sort_by: sort_by_str.parse().unwrap_or_default(),
@@ -387,26 +383,30 @@ pub async fn get_activity_feed(
filter_following,
};
match application::use_cases::get_activity_feed::execute(&state.app_ctx, query).await {
match application::diary::get_activity_feed::execute(&state.app_ctx, query).await {
Ok(entries) => {
let entry_limit = entries.limit;
let entry_offset = entries.offset;
let has_more =
(entry_offset as u64).saturating_add(entry_limit as u64) < entries.total_count;
let data = application::ports::ActivityFeedPageData {
ctx,
let total_pages = (entries.total_count as u32)
.saturating_add(entry_limit.saturating_sub(1))
.checked_div(entry_limit)
.unwrap_or(1);
let current_page = entry_offset.checked_div(entry_limit).unwrap_or(0);
let page_items = build_page_items(total_pages, current_page);
render_page(ActivityFeedTemplate {
entries: entries.items.as_slice(),
current_offset: entry_offset,
has_more,
limit: entry_limit,
entries,
has_more,
ctx: &ctx,
page_items,
filter: filter_str.to_string(),
sort_by: sort_by_str.to_string(),
search: params.search,
};
match state.html_renderer.render_activity_feed_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
})
.into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
@@ -421,32 +421,54 @@ 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);
match application::use_cases::get_users::execute(
match application::users::get_users::execute(
&state.app_ctx,
application::queries::GetUsersQuery,
application::users::queries::GetUsersQuery,
)
.await
{
Ok(result) => {
let actor_views = result
.remote_actors
let users: Vec<UserSummaryView> = result
.users
.into_iter()
.map(|a| application::ports::RemoteActorView {
handle: a.handle,
display_name: a.display_name,
url: a.url,
avatar_url: None,
.map(|u| {
let name = u.email().split('@').next().unwrap_or("?").to_string();
let initial = name.chars().next().unwrap_or('?').to_ascii_uppercase();
let avg_display = u
.avg_rating
.map(|r| format!("{:.1}", r))
.unwrap_or_else(|| "".to_string());
let avatar_url = u.avatar_path.map(|p| format!("/images/{}", p));
UserSummaryView {
user_id: u.user_id.value(),
display_name: name,
initial,
avg_rating_display: avg_display,
total_movies: u.total_movies,
avatar_url,
}
})
.collect();
let data = application::ports::UsersPageData {
ctx,
users: result.users,
remote_actors: actor_views,
};
match state.html_renderer.render_users_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
let remote_actors: Vec<RemoteActorDisplay> = result
.remote_actors
.into_iter()
.map(|a| {
let display = a.display_name.unwrap_or_else(|| a.handle.clone());
let initial = display.chars().next().unwrap_or('?').to_ascii_uppercase();
RemoteActorDisplay {
handle: a.handle,
display_name: display,
initial,
url: a.url,
}
})
.collect();
render_page(UsersTemplate {
users,
ctx: &ctx,
remote_actors,
})
.into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
@@ -460,7 +482,7 @@ pub async fn get_user_by_username(
Ok(u) => u,
Err(_) => return StatusCode::NOT_FOUND.into_response(),
};
match state.app_ctx.user_repository.find_by_username(&uname).await {
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()
@@ -505,7 +527,7 @@ pub async fn get_user_profile(
let mut ctx = build_page_context(&state, user_id.clone(), csrf.0).await;
let view_str = params.view.as_deref().unwrap_or("recent");
let profile_view = match application::queries::ProfileView::from_str(view_str) {
let profile_view = match application::users::queries::ProfileView::from_str(view_str) {
Ok(v) => v,
Err(_) => {
return (
@@ -518,7 +540,8 @@ pub async fn get_user_profile(
let profile_user = match state
.app_ctx
.user_repository
.repos
.user
.find_by_id(&domain::value_objects::UserId::from_uuid(profile_user_uuid))
.await
{
@@ -546,7 +569,7 @@ pub async fn get_user_profile(
.map(|u| u.value() == profile_user_uuid)
.unwrap_or(false);
let query = application::queries::GetUserProfileQuery {
let query = application::users::queries::GetUserProfileQuery {
user_id: profile_user_uuid,
view: profile_view,
limit: params.limit,
@@ -560,7 +583,7 @@ pub async fn get_user_profile(
is_own_profile,
};
match application::use_cases::get_user_profile::execute(&state.app_ctx, query).await {
match application::users::get_profile::execute(&state.app_ctx, query).await {
Ok(profile) => {
let (offset, has_more, limit) = profile
.entries
@@ -573,28 +596,80 @@ pub async fn get_user_profile(
if !is_own_profile {
ctx.page_rss_url = Some(format!("/users/{}/feed.rss", profile_user_uuid));
}
let pending_followers: Vec<application::ports::RemoteActorView> = profile
let email = profile_user.email().value().to_string();
let display_name = email.split('@').next().unwrap_or("?").to_string();
let avg_rating_display = profile
.stats
.avg_rating
.map(|r| format!("{:.1}", r))
.unwrap_or_else(|| "".to_string());
let favorite_director_display = profile
.stats
.favorite_director
.clone()
.unwrap_or_else(|| "".to_string());
let most_active_month_display = profile
.stats
.most_active_month
.clone()
.unwrap_or_else(|| "".to_string());
let heatmap = profile
.history
.as_deref()
.map(build_heatmap)
.unwrap_or_default();
let monthly_rating_rows: Vec<MonthlyRatingRow<'_>> = profile
.trends
.as_ref()
.map(|t| {
t.monthly_ratings
.iter()
.map(|r| MonthlyRatingRow {
rating: r,
bar_height_px: bar_height_px(r.avg_rating),
})
.collect()
})
.unwrap_or_default();
let total = profile
.entries
.as_ref()
.map(|e| e.total_count as u32)
.unwrap_or(0);
let total_pages = total
.saturating_add(limit.saturating_sub(1))
.checked_div(limit)
.unwrap_or(1);
let current_page = offset.checked_div(limit).unwrap_or(0);
let page_items = build_page_items(total_pages, current_page);
let pending_followers: Vec<RemoteActorData> = profile
.pending_followers
.into_iter()
.map(|p| application::ports::RemoteActorView {
.map(|p| RemoteActorData {
handle: p.handle,
url: p.url,
display_name: p.display_name,
avatar_url: p.avatar_url,
})
.collect();
let data = application::ports::ProfilePageData {
ctx,
render_page(ProfileTemplate {
ctx: &ctx,
profile_display_name: display_name,
profile_user_id: profile_user_uuid,
profile_user_email: profile_user.email().value().to_string(),
stats: profile.stats,
view: profile_view.as_str().to_string(),
entries: profile.entries,
stats: &profile.stats,
avg_rating_display,
favorite_director_display,
most_active_month_display,
view: profile_view.as_str(),
entries: profile.entries.as_ref(),
current_offset: offset,
has_more,
limit,
history: profile.history,
trends: profile.trends,
history: profile.history.as_ref(),
trends: profile.trends.as_ref(),
monthly_rating_rows,
heatmap,
page_items,
is_own_profile,
error: params.error,
following_count: profile.following_count,
@@ -602,11 +677,8 @@ pub async fn get_user_profile(
pending_followers,
sort_by: sort_by_str.to_string(),
search: params.search.clone(),
};
match state.html_renderer.render_profile_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
})
.into_response()
}
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
@@ -818,25 +890,22 @@ pub async fn get_following_page(
);
match state.ap_service.get_following(user_id.value()).await {
Ok(following) => {
let actors = following
let actors: Vec<RemoteActorData> = following
.into_iter()
.map(|a| RemoteActorView {
.map(|a| RemoteActorData {
handle: a.handle,
display_name: a.display_name,
url: a.url,
avatar_url: a.avatar_url.clone(),
})
.collect();
let data = FollowingPageData {
render_page(FollowingTemplate {
ctx,
user_id: profile_user_uuid,
actors,
error: params.error,
};
match state.html_renderer.render_following_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
})
.into_response()
}
Err(e) => {
tracing::error!("get_following error: {:?}", e);
@@ -872,25 +941,22 @@ pub async fn get_followers_page(
.await
{
Ok(followers) => {
let actors = followers
let actors: Vec<RemoteActorData> = followers
.into_iter()
.map(|a| RemoteActorView {
.map(|a| RemoteActorData {
handle: a.handle,
display_name: a.display_name,
url: a.url,
avatar_url: a.avatar_url.clone(),
})
.collect();
let data = FollowersPageData {
render_page(FollowersTemplate {
ctx,
user_id: profile_user_uuid,
actors,
error: params.error,
};
match state.html_renderer.render_followers_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
})
.into_response()
}
Err(e) => {
tracing::error!("get_followers error: {:?}", e);
@@ -985,25 +1051,21 @@ pub async fn get_movie_detail(
.unwrap_or(false),
None => false,
};
let data = MovieDetailPageData {
ctx,
movie: result.movie,
stats: result.stats,
profile: result.profile,
let current_offset = result.reviews.offset;
let reviews_limit = result.reviews.limit;
render_page(MovieDetailTemplate {
ctx: &ctx,
movie: &result.movie,
stats: &result.stats,
profile: result.profile.as_ref(),
reviews: result.reviews.items.as_slice(),
on_watchlist,
current_offset: result.reviews.offset,
current_offset,
has_more,
limit: result.reviews.limit,
reviews: result.reviews,
limit: reviews_limit,
histogram_max,
};
match state.html_renderer.render_movie_detail_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => {
tracing::error!("template error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
})
.into_response()
}
}
}
@@ -1018,9 +1080,9 @@ 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 result = match application::use_cases::get_watchlist_page::execute(
let result = match application::watchlist::get_page::execute(
&state.app_ctx,
application::queries::GetWatchlistQuery {
application::watchlist::queries::GetWatchlistQuery {
user_id: owner_id,
limit: params.limit.or(Some(20)),
offset: params.offset.or(Some(0)),
@@ -1036,23 +1098,17 @@ pub async fn get_watchlist_page(
}
};
let data = WatchlistPageData {
ctx,
render_page(WatchlistTemplate {
ctx: &ctx,
owner_id,
display_entries: result.display_entries,
display_entries: &result.display_entries,
current_offset: result.current_offset,
has_more: result.has_more,
limit: result.limit,
is_owner,
error: params.error,
};
match state.html_renderer.render_watchlist_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => {
tracing::error!("watchlist template error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
})
.into_response()
}
pub async fn post_watchlist_add(
@@ -1180,7 +1236,7 @@ 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.user_repository.find_by_id(&user_id).await {
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(),
Err(e) => {
@@ -1197,9 +1253,10 @@ pub async fn get_profile_settings(
.banner_path()
.map(|path| format!("{}/images/{}", base_url, path));
let profile_fields = state
let profile_fields: Vec<(String, String)> = state
.app_ctx
.profile_fields_repository
.repos
.profile_fields
.get_fields(&user_id)
.await
.unwrap_or_default()
@@ -1209,23 +1266,19 @@ pub async fn get_profile_settings(
let saved = params.saved.as_deref() == Some("1");
let data = ProfileSettingsPageData {
ctx,
bio: user.bio().map(|s| s.to_string()),
avatar_url,
banner_url,
also_known_as: user.also_known_as().map(|s| s.to_string()),
profile_fields,
saved,
};
let bio = user.bio().map(|s| s.to_string());
let also_known_as = user.also_known_as().map(|s| s.to_string());
match state.html_renderer.render_profile_settings_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => {
tracing::error!("profile_settings template error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
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,
saved,
})
.into_response()
}
pub async fn get_tag(Path(tag): Path<String>) -> impl IntoResponse {
@@ -1247,21 +1300,19 @@ pub async fn get_blocked_domains_page(
ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url);
match state.ap_service.get_blocked_domains().await {
Ok(domains) => {
let data = BlockedDomainsPageData {
ctx,
domains: domains
.into_iter()
.map(|d| BlockedDomainEntry {
domain: d.domain,
reason: d.reason,
blocked_at: d.blocked_at,
})
.collect(),
};
match state.html_renderer.render_blocked_domains_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
let entries: Vec<template_askama::BlockedDomainEntry> = domains
.into_iter()
.map(|d| template_askama::BlockedDomainEntry {
domain: d.domain,
reason: d.reason,
blocked_at: d.blocked_at,
})
.collect();
render_page(BlockedDomainsTemplate {
ctx: &ctx,
domains: &entries,
})
.into_response()
}
Err(e) => {
tracing::error!("get_blocked_domains error: {:?}", e);
@@ -1328,22 +1379,20 @@ pub async fn get_blocked_actors_page(
ctx.canonical_url = format!("{}/social/blocked", state.app_ctx.config.base_url);
match state.ap_service.get_blocked_actors(user_id.value()).await {
Ok(actors) => {
let data = BlockedActorsPageData {
ctx,
actors: actors
.into_iter()
.map(|a| BlockedActorEntry {
url: a.url,
handle: a.handle,
display_name: a.display_name,
avatar_url: a.avatar_url,
})
.collect(),
};
match state.html_renderer.render_blocked_actors_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
let entries: Vec<template_askama::BlockedActorEntry> = actors
.into_iter()
.map(|a| template_askama::BlockedActorEntry {
url: a.url,
handle: a.handle,
display_name: a.display_name,
avatar_url: a.avatar_url,
})
.collect();
render_page(BlockedActorsTemplate {
ctx: &ctx,
actors: &entries,
})
.into_response()
}
Err(e) => {
tracing::error!("get_blocked_actors error: {:?}", e);
@@ -1475,7 +1524,7 @@ pub async fn post_profile_settings(
}
}
let cmd = application::commands::UpdateProfileCommand {
let cmd = application::users::commands::UpdateProfileCommand {
user_id: user_id.value(),
display_name,
bio,
@@ -1498,7 +1547,7 @@ pub async fn post_profile_settings(
})
.collect();
let fields_cmd = application::commands::UpdateProfileFieldsCommand {
let fields_cmd = application::users::commands::UpdateProfileFieldsCommand {
user_id: user_id.value(),
fields,
};
@@ -1526,9 +1575,9 @@ pub async fn get_integrations_page(
.await
.unwrap_or_default();
let token_views: Vec<WebhookTokenView> = tokens
let token_views: Vec<template_askama::WebhookTokenView> = tokens
.into_iter()
.map(|t| WebhookTokenView {
.map(|t| template_askama::WebhookTokenView {
id: t.id().value().to_string(),
provider: t.provider().to_string(),
label: t.label().map(String::from),
@@ -1539,20 +1588,14 @@ pub async fn get_integrations_page(
})
.collect();
let data = IntegrationsPageData {
ctx,
tokens: token_views,
webhook_base_url: state.app_ctx.config.base_url.clone(),
new_token: params.token,
};
match state.html_renderer.render_integrations_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => {
tracing::error!("integrations template error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
let webhook_base_url = state.app_ctx.config.base_url.clone();
render_page(IntegrationsTemplate {
ctx: &ctx,
tokens: &token_views,
webhook_base_url: &webhook_base_url,
new_token: params.token.as_deref(),
})
.into_response()
}
pub async fn post_generate_token(
@@ -1632,9 +1675,9 @@ pub async fn get_watch_queue_page(
.await
.unwrap_or_default();
let entries: Vec<WatchQueueDisplayEntry> = events
let entries: Vec<template_askama::WatchQueueDisplayEntry> = events
.into_iter()
.map(|e| WatchQueueDisplayEntry {
.map(|e| template_askama::WatchQueueDisplayEntry {
id: e.id().value().to_string(),
title: e.title().to_string(),
year: e.year(),
@@ -1644,19 +1687,12 @@ pub async fn get_watch_queue_page(
})
.collect();
let data = WatchQueuePageData {
ctx,
entries,
error: params.error,
};
match state.html_renderer.render_watch_queue_page(data) {
Ok(html) => Html(html).into_response(),
Err(e) => {
tracing::error!("watch_queue template error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
render_page(WatchQueueTemplate {
ctx: &ctx,
entries: &entries,
error: params.error.as_deref(),
})
.into_response()
}
pub async fn post_confirm_single(

View File

@@ -11,25 +11,26 @@ use axum::{
use serde::Deserialize;
use std::collections::HashMap;
use application::{
use crate::render::render_page;
use application::import::{
apply_mapping as apply_import_mapping,
commands::{
ApplyImportMappingCommand, CreateImportSessionCommand, DeleteImportProfileCommand,
ExecuteImportCommand, SaveImportProfileCommand,
},
ports::{
ImportMappingPageData, ImportPreviewPageData, ImportPreviewRow, ImportProfileView,
ImportRowStatus, ImportUploadPageData,
},
use_cases::{
apply_import_mapping, create_import_session, delete_import_profile, execute_import,
list_import_profiles, save_import_profile,
},
create_session as create_import_session, delete_profile as delete_import_profile,
execute as execute_import, list_profiles as list_import_profiles,
save_profile as save_import_profile,
};
use domain::models::{
AnnotatedRow, FieldMapping, FileFormat,
import::{DomainField, RowResult, Transform},
};
use domain::value_objects::ImportSessionId;
use template_askama::{
ImportMappingTemplate, ImportPreviewRow, ImportPreviewTemplate, ImportProfileView,
ImportRowStatus, ImportUploadTemplate,
};
use crate::{
csrf::CsrfToken,
@@ -143,15 +144,11 @@ pub async fn get_import_page(
name: p.name,
})
.collect::<Vec<_>>();
let html = state
.html_renderer
.render_import_upload_page(ImportUploadPageData {
ctx,
profiles,
error: None,
})
.unwrap_or_else(|e| e);
Html(html)
render_page(ImportUploadTemplate {
ctx: &ctx,
profiles: &profiles,
error: None,
})
}
pub async fn post_upload(
@@ -220,7 +217,8 @@ pub async fn get_mapping_page(
};
let Ok(Some(session)) = state
.app_ctx
.import_session_repository
.repos
.import_session
.get(&session_id, &user_id)
.await
else {
@@ -231,27 +229,25 @@ pub async fn get_mapping_page(
};
let ctx = super::html::build_page_context(&state, Some(user_id), csrf.0).await;
let sample_rows = parsed.rows.into_iter().take(5).collect();
let html = state
.html_renderer
.render_import_mapping_page(ImportMappingPageData {
ctx,
session_id: session_id_str,
columns: parsed.columns,
sample_rows,
domain_fields: vec![
("title", "Title"),
("release_year", "Release Year"),
("director", "Director"),
("rating", "Rating"),
("watched_at", "Watched At"),
("comment", "Comment"),
("external_metadata_id", "External ID"),
],
error: None,
})
.unwrap_or_else(|e| e);
Html(html).into_response()
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"),
("director", "Director"),
("rating", "Rating"),
("watched_at", "Watched At"),
("comment", "Comment"),
("external_metadata_id", "External ID"),
];
render_page(ImportMappingTemplate {
ctx: &ctx,
session_id: &session_id_str,
columns: &parsed.columns,
sample_rows: &sample_rows,
domain_fields: &domain_fields,
error: None,
})
.into_response()
}
pub async fn post_mapping(
@@ -313,7 +309,8 @@ pub async fn get_preview_page(
};
let Ok(Some(session)) = state
.app_ctx
.import_session_repository
.repos
.import_session
.get(&session_id, &user_id)
.await
else {
@@ -334,16 +331,13 @@ pub async fn get_preview_page(
.collect();
let ctx = super::html::build_page_context(&state, Some(user_id), csrf.0).await;
let html = state
.html_renderer
.render_import_preview_page(ImportPreviewPageData {
ctx,
session_id: session_id_str,
columns: parsed.columns,
rows,
})
.unwrap_or_else(|e| e);
Html(html).into_response()
render_page(ImportPreviewTemplate {
ctx: &ctx,
session_id: &session_id_str,
columns: &parsed.columns,
rows: &rows,
})
.into_response()
}
pub async fn post_confirm(
@@ -571,7 +565,8 @@ pub async fn api_get_session(
};
match state
.app_ctx
.import_session_repository
.repos
.import_session
.get(&session_id, &user_id)
.await
{

View File

@@ -5,7 +5,7 @@ use axum::{
};
use uuid::Uuid;
use application::{queries::GetDiaryQuery, use_cases::get_diary};
use application::{diary::get_diary, diary::queries::GetDiaryQuery};
use domain::{errors::DomainError, models::SortDirection, value_objects::UserId};
use crate::{errors::ApiError, state::AppState};
@@ -35,7 +35,8 @@ pub async fn get_user_feed(
) -> Result<impl IntoResponse, ApiError> {
let user = state
.app_ctx
.user_repository
.repos
.user
.find_by_id(&UserId::from_uuid(user_id))
.await
.map_err(ApiError)?

View File

@@ -10,16 +10,16 @@ use api_types::{
ConfirmWatchRequest, ConfirmWatchResponse, DismissWatchRequest, DismissWatchResponse,
GenerateTokenRequest, GenerateTokenResponse, WatchQueueEntryDto, WebhookTokenDto,
};
use application::{
use application::integrations::{
commands::{
ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand,
IngestWatchEventCommand, RevokeWebhookTokenCommand, WatchEventConfirmation,
},
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},
use_cases::{
confirm_watch_events, dismiss_watch_events, generate_webhook_token, get_watch_queue,
get_webhook_tokens, ingest_watch_event, revoke_webhook_token,
},
revoke_token as revoke_webhook_token,
};
use domain::models::WatchEventSource;