This commit is contained in:
@@ -2,7 +2,10 @@ use chrono::NaiveDateTime;
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{commands::{LogReviewCommand, MovieInput}, queries::GetDiaryQuery};
|
||||
use application::{
|
||||
commands::{LogReviewCommand, MovieInput},
|
||||
queries::GetDiaryQuery,
|
||||
};
|
||||
use domain::{errors::DomainError, models::SortDirection};
|
||||
|
||||
use api_types::{DiaryQueryParams, LogReviewRequest};
|
||||
|
||||
@@ -10,55 +10,57 @@ use std::str::FromStr;
|
||||
|
||||
use application::{
|
||||
commands::{
|
||||
DeleteReviewCommand, MovieInput, RegisterCommand, SyncPosterCommand,
|
||||
AddToWatchlistCommand, RemoveFromWatchlistCommand,
|
||||
AddToWatchlistCommand, DeleteReviewCommand, MovieInput, RegisterCommand,
|
||||
RemoveFromWatchlistCommand, SyncPosterCommand,
|
||||
},
|
||||
queries::{
|
||||
ExportQuery, GetActivityFeedQuery, GetMovieSocialPageQuery, GetMoviesQuery,
|
||||
GetReviewHistoryQuery, GetUserProfileQuery, GetUsersQuery, LoginQuery,
|
||||
GetWatchlistQuery, IsOnWatchlistQuery,
|
||||
GetReviewHistoryQuery, GetUserProfileQuery, GetUsersQuery, GetWatchlistQuery,
|
||||
IsOnWatchlistQuery, LoginQuery,
|
||||
},
|
||||
use_cases::{
|
||||
delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc,
|
||||
get_diary, get_movie_social_page, get_movies, get_review_history,
|
||||
get_user_profile as get_user_profile_uc, get_users, log_review, login as login_uc,
|
||||
register as register_uc, sync_poster, update_profile, update_profile_fields,
|
||||
search as search_uc, get_person, get_person_credits,
|
||||
add_to_watchlist, remove_from_watchlist, get_watchlist, is_on_watchlist,
|
||||
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,
|
||||
},
|
||||
};
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::{DiaryEntry, ExportFormat, Movie, MovieSummary, Review, PersonId, collections::PageParams},
|
||||
models::{
|
||||
DiaryEntry, ExportFormat, Movie, MovieSummary, PersonId, Review, collections::PageParams,
|
||||
},
|
||||
services::review_history::Trend,
|
||||
value_objects::{MovieId, UserId},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::AuthenticatedUser,
|
||||
forms::{LogReviewData, to_diary_query},
|
||||
state::AppState,
|
||||
};
|
||||
use api_types::search::{
|
||||
CastCreditDto, CrewCreditDto, MovieSearchHitDto, PaginatedMovieHits, PaginatedPersonHits,
|
||||
PersonCreditsDto, PersonDto, PersonSearchHitDto, SearchQueryParams, SearchResponse,
|
||||
};
|
||||
use api_types::{
|
||||
ActivityFeedQueryParams, ActivityFeedResponse, AddToWatchlistRequest, CastMemberDto,
|
||||
CrewMemberDto, DiaryEntryDto, DiaryQueryParams, DiaryResponse, DirectorStatDto,
|
||||
ExportQueryParams, FeedEntryDto, GenreDto, KeywordDto, LogReviewRequest, LoginRequest,
|
||||
LoginResponse, MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto,
|
||||
MovieProfileResponse, MovieStatsDto, MoviesQueryParams, MoviesResponse, PaginationQueryParams,
|
||||
ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse, SocialFeedResponse,
|
||||
SocialReviewDto, UserProfileQueryParams, UserProfileResponse, UserStatsDto, UserSummaryDto,
|
||||
UserTrendsDto, UsersResponse, WatchlistEntryDto, WatchlistResponse, WatchlistStatusResponse,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use api_types::{
|
||||
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
|
||||
BlockedDomainResponse, FollowRequest, RemoteActorDto,
|
||||
};
|
||||
use api_types::{
|
||||
ActivityFeedQueryParams, ActivityFeedResponse, CastMemberDto, CrewMemberDto, DiaryEntryDto,
|
||||
DiaryQueryParams, DiaryResponse, DirectorStatDto, ExportQueryParams, FeedEntryDto,
|
||||
GenreDto, KeywordDto, LogReviewRequest, LoginRequest, LoginResponse, MonthActivityDto,
|
||||
MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieProfileResponse, MovieStatsDto,
|
||||
MoviesQueryParams, MoviesResponse, PaginationQueryParams, ProfileResponse, RegisterRequest,
|
||||
ReviewDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto, UserProfileQueryParams,
|
||||
UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto, UsersResponse,
|
||||
WatchlistResponse, WatchlistEntryDto, WatchlistStatusResponse, AddToWatchlistRequest,
|
||||
};
|
||||
use api_types::search::{
|
||||
CastCreditDto, CrewCreditDto, MovieSearchHitDto, PersonCreditsDto, PersonDto,
|
||||
PersonSearchHitDto, PaginatedMovieHits, PaginatedPersonHits, SearchQueryParams, SearchResponse,
|
||||
};
|
||||
use crate::{
|
||||
errors::ApiError,
|
||||
extractors::AuthenticatedUser,
|
||||
forms::{to_diary_query, LogReviewData},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/diary",
|
||||
@@ -307,7 +309,11 @@ pub async fn get_movie_detail(
|
||||
|
||||
let result = get_movie_social_page::execute(
|
||||
&state.app_ctx,
|
||||
GetMovieSocialPageQuery { movie_id, limit, offset },
|
||||
GetMovieSocialPageQuery {
|
||||
movie_id,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -320,13 +326,18 @@ pub async fn get_movie_detail(
|
||||
rating_histogram: result.stats.rating_histogram,
|
||||
},
|
||||
reviews: SocialFeedResponse {
|
||||
items: result.reviews.items.iter().map(|e| SocialReviewDto {
|
||||
user_display: e.user_display_name().to_string(),
|
||||
rating: e.review().rating().value(),
|
||||
comment: e.review().comment().map(|c| c.value().to_string()),
|
||||
watched_at: e.review().watched_at().to_string(),
|
||||
is_federated: e.review().is_remote(),
|
||||
}).collect(),
|
||||
items: result
|
||||
.reviews
|
||||
.items
|
||||
.iter()
|
||||
.map(|e| SocialReviewDto {
|
||||
user_display: e.user_display_name().to_string(),
|
||||
rating: e.review().rating().value(),
|
||||
comment: e.review().comment().map(|c| c.value().to_string()),
|
||||
watched_at: e.review().watched_at().to_string(),
|
||||
is_federated: e.review().is_remote(),
|
||||
})
|
||||
.collect(),
|
||||
total_count: result.reviews.total_count,
|
||||
limit: result.reviews.limit,
|
||||
offset: result.reviews.offset,
|
||||
@@ -347,7 +358,12 @@ 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
|
||||
.movie_profile_repository
|
||||
.get_by_movie_id(&id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(p)) => Json(MovieProfileResponse {
|
||||
tmdb_id: p.tmdb_id,
|
||||
imdb_id: p.imdb_id,
|
||||
@@ -360,18 +376,47 @@ pub async fn get_movie_profile(
|
||||
vote_count: p.vote_count,
|
||||
original_language: p.original_language,
|
||||
collection_name: p.collection_name,
|
||||
genres: p.genres.into_iter().map(|g| GenreDto { tmdb_id: g.tmdb_id, name: g.name }).collect(),
|
||||
keywords: p.keywords.into_iter().map(|k| KeywordDto { tmdb_id: k.tmdb_id, name: k.name }).collect(),
|
||||
cast: p.cast.into_iter().map(|c| CastMemberDto {
|
||||
tmdb_person_id: c.tmdb_person_id, name: c.name, character: c.character,
|
||||
billing_order: c.billing_order, profile_path: c.profile_path,
|
||||
}).collect(),
|
||||
crew: p.crew.into_iter().map(|c| CrewMemberDto {
|
||||
tmdb_person_id: c.tmdb_person_id, name: c.name, job: c.job,
|
||||
department: c.department, profile_path: c.profile_path,
|
||||
}).collect(),
|
||||
genres: p
|
||||
.genres
|
||||
.into_iter()
|
||||
.map(|g| GenreDto {
|
||||
tmdb_id: g.tmdb_id,
|
||||
name: g.name,
|
||||
})
|
||||
.collect(),
|
||||
keywords: p
|
||||
.keywords
|
||||
.into_iter()
|
||||
.map(|k| KeywordDto {
|
||||
tmdb_id: k.tmdb_id,
|
||||
name: k.name,
|
||||
})
|
||||
.collect(),
|
||||
cast: p
|
||||
.cast
|
||||
.into_iter()
|
||||
.map(|c| CastMemberDto {
|
||||
tmdb_person_id: c.tmdb_person_id,
|
||||
name: c.name,
|
||||
character: c.character,
|
||||
billing_order: c.billing_order,
|
||||
profile_path: c.profile_path,
|
||||
})
|
||||
.collect(),
|
||||
crew: p
|
||||
.crew
|
||||
.into_iter()
|
||||
.map(|c| CrewMemberDto {
|
||||
tmdb_person_id: c.tmdb_person_id,
|
||||
name: c.name,
|
||||
job: c.job,
|
||||
department: c.department,
|
||||
profile_path: c.profile_path,
|
||||
})
|
||||
.collect(),
|
||||
enriched_at: p.enriched_at.to_rfc3339(),
|
||||
}).into_response(),
|
||||
})
|
||||
.into_response(),
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("get_movie_profile: {:?}", e);
|
||||
@@ -440,7 +485,11 @@ pub async fn update_profile_handler(
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
match name.as_str() {
|
||||
"bio" => { if let Ok(text) = field.text().await { bio = Some(text); } }
|
||||
"bio" => {
|
||||
if let Ok(text) = field.text().await {
|
||||
bio = Some(text);
|
||||
}
|
||||
}
|
||||
"also_known_as" => {
|
||||
if let Ok(text) = field.text().await {
|
||||
also_known_as = Some(text).filter(|s| !s.is_empty());
|
||||
@@ -449,13 +498,19 @@ pub async fn update_profile_handler(
|
||||
"avatar" => {
|
||||
let ct = field.content_type().map(|s| s.to_string());
|
||||
if let Ok(bytes) = field.bytes().await {
|
||||
if !bytes.is_empty() { avatar_bytes = Some(bytes.to_vec()); avatar_content_type = ct; }
|
||||
if !bytes.is_empty() {
|
||||
avatar_bytes = Some(bytes.to_vec());
|
||||
avatar_content_type = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
"banner" => {
|
||||
let ct = field.content_type().map(|s| s.to_string());
|
||||
if let Ok(bytes) = field.bytes().await {
|
||||
if !bytes.is_empty() { banner_bytes = Some(bytes.to_vec()); banner_content_type = ct; }
|
||||
if !bytes.is_empty() {
|
||||
banner_bytes = Some(bytes.to_vec());
|
||||
banner_content_type = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -613,7 +668,11 @@ pub async fn add_blocked_domain_admin(
|
||||
_admin: crate::extractors::AdminUser,
|
||||
axum::Json(body): axum::Json<AddBlockedDomainRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match state.ap_service.add_blocked_domain(&body.domain, body.reason.as_deref()).await {
|
||||
match state
|
||||
.ap_service
|
||||
.add_blocked_domain(&body.domain, body.reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => StatusCode::CREATED.into_response(),
|
||||
Err(e) => ap_err(e).into_response(),
|
||||
}
|
||||
@@ -656,7 +715,11 @@ pub async fn block_actor_api(
|
||||
user: AuthenticatedUser,
|
||||
axum::Json(body): axum::Json<ActorUrlRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match state.ap_service.block_actor(user.0.value(), &body.actor_url).await {
|
||||
match state
|
||||
.ap_service
|
||||
.block_actor(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
{
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(e) => ap_err(e).into_response(),
|
||||
}
|
||||
@@ -677,7 +740,11 @@ pub async fn unblock_actor_api(
|
||||
user: AuthenticatedUser,
|
||||
axum::Json(body): axum::Json<ActorUrlRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match state.ap_service.unblock_actor(user.0.value(), &body.actor_url).await {
|
||||
match state
|
||||
.ap_service
|
||||
.unblock_actor(user.0.value(), &body.actor_url)
|
||||
.await
|
||||
{
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(e) => ap_err(e).into_response(),
|
||||
}
|
||||
@@ -972,9 +1039,7 @@ pub async fn get_activity_feed(
|
||||
get, path = "/api/v1/users",
|
||||
responses((status = 200, body = UsersResponse)),
|
||||
)]
|
||||
pub async fn list_users(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<UsersResponse>, ApiError> {
|
||||
pub async fn list_users(State(state): State<AppState>) -> Result<Json<UsersResponse>, ApiError> {
|
||||
let users = get_users::execute(&state.app_ctx, GetUsersQuery).await?;
|
||||
Ok(Json(UsersResponse {
|
||||
users: users
|
||||
@@ -1199,31 +1264,42 @@ pub async fn get_search(
|
||||
match search_uc::execute(&state.app_ctx, query).await {
|
||||
Ok(results) => axum::Json(SearchResponse {
|
||||
movies: PaginatedMovieHits {
|
||||
items: results.movies.items.iter().map(|h| MovieSearchHitDto {
|
||||
movie_id: h.movie_id.value(),
|
||||
title: h.title.clone(),
|
||||
release_year: h.release_year,
|
||||
director: h.director.clone(),
|
||||
poster_path: h.poster_path.clone(),
|
||||
genres: h.genres.clone(),
|
||||
}).collect(),
|
||||
items: results
|
||||
.movies
|
||||
.items
|
||||
.iter()
|
||||
.map(|h| MovieSearchHitDto {
|
||||
movie_id: h.movie_id.value(),
|
||||
title: h.title.clone(),
|
||||
release_year: h.release_year,
|
||||
director: h.director.clone(),
|
||||
poster_path: h.poster_path.clone(),
|
||||
genres: h.genres.clone(),
|
||||
})
|
||||
.collect(),
|
||||
total_count: results.movies.total_count,
|
||||
limit: results.movies.limit,
|
||||
offset: results.movies.offset,
|
||||
},
|
||||
people: PaginatedPersonHits {
|
||||
items: results.people.items.iter().map(|h| PersonSearchHitDto {
|
||||
person_id: h.person_id.value(),
|
||||
name: h.name.clone(),
|
||||
known_for_department: h.known_for_department.clone(),
|
||||
profile_path: h.profile_path.clone(),
|
||||
known_for_titles: h.known_for_titles.clone(),
|
||||
}).collect(),
|
||||
items: results
|
||||
.people
|
||||
.items
|
||||
.iter()
|
||||
.map(|h| PersonSearchHitDto {
|
||||
person_id: h.person_id.value(),
|
||||
name: h.name.clone(),
|
||||
known_for_department: h.known_for_department.clone(),
|
||||
profile_path: h.profile_path.clone(),
|
||||
known_for_titles: h.known_for_titles.clone(),
|
||||
})
|
||||
.collect(),
|
||||
total_count: results.people.total_count,
|
||||
limit: results.people.limit,
|
||||
offset: results.people.offset,
|
||||
},
|
||||
}).into_response(),
|
||||
})
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("search failed: {e}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
@@ -1251,7 +1327,8 @@ pub async fn get_person_handler(
|
||||
name: person.name().to_string(),
|
||||
known_for_department: person.known_for_department().map(str::to_string),
|
||||
profile_path: person.profile_path().map(str::to_string),
|
||||
}).into_response(),
|
||||
})
|
||||
.into_response(),
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("get_person failed: {e}");
|
||||
@@ -1282,22 +1359,31 @@ pub async fn get_person_credits_handler(
|
||||
known_for_department: credits.person.known_for_department().map(str::to_string),
|
||||
profile_path: credits.person.profile_path().map(str::to_string),
|
||||
},
|
||||
cast: credits.cast.iter().map(|c| CastCreditDto {
|
||||
movie_id: c.movie_id.value(),
|
||||
title: c.title.clone(),
|
||||
release_year: c.release_year,
|
||||
character: c.character.clone(),
|
||||
poster_path: c.poster_path.clone(),
|
||||
}).collect(),
|
||||
crew: credits.crew.iter().map(|c| CrewCreditDto {
|
||||
movie_id: c.movie_id.value(),
|
||||
title: c.title.clone(),
|
||||
release_year: c.release_year,
|
||||
job: c.job.clone(),
|
||||
department: c.department.clone(),
|
||||
poster_path: c.poster_path.clone(),
|
||||
}).collect(),
|
||||
}).into_response(),
|
||||
cast: credits
|
||||
.cast
|
||||
.iter()
|
||||
.map(|c| CastCreditDto {
|
||||
movie_id: c.movie_id.value(),
|
||||
title: c.title.clone(),
|
||||
release_year: c.release_year,
|
||||
character: c.character.clone(),
|
||||
poster_path: c.poster_path.clone(),
|
||||
})
|
||||
.collect(),
|
||||
crew: credits
|
||||
.crew
|
||||
.iter()
|
||||
.map(|c| CrewCreditDto {
|
||||
movie_id: c.movie_id.value(),
|
||||
title: c.title.clone(),
|
||||
release_year: c.release_year,
|
||||
job: c.job.clone(),
|
||||
department: c.department.clone(),
|
||||
poster_path: c.poster_path.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.into_response(),
|
||||
Err(DomainError::NotFound(_)) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("get_person_credits failed: {e}");
|
||||
@@ -1334,11 +1420,15 @@ pub async fn get_watchlist_handler(
|
||||
.await?;
|
||||
|
||||
Ok(Json(WatchlistResponse {
|
||||
items: page.items.into_iter().map(|w| WatchlistEntryDto {
|
||||
id: w.entry.id.value(),
|
||||
movie: movie_to_dto(&w.movie),
|
||||
added_at: w.entry.added_at.to_string(),
|
||||
}).collect(),
|
||||
items: page
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|w| WatchlistEntryDto {
|
||||
id: w.entry.id.value(),
|
||||
movie: movie_to_dto(&w.movie),
|
||||
added_at: w.entry.added_at.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
total_count: page.total_count,
|
||||
limit: page.limit,
|
||||
offset: page.offset,
|
||||
@@ -1394,7 +1484,10 @@ pub async fn delete_watchlist_entry(
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
remove_from_watchlist::execute(
|
||||
&state.app_ctx,
|
||||
RemoveFromWatchlistCommand { user_id: user.0.value(), movie_id },
|
||||
RemoveFromWatchlistCommand {
|
||||
user_id: user.0.value(),
|
||||
movie_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
@@ -1416,7 +1509,10 @@ pub async fn get_watchlist_status(
|
||||
) -> Result<Json<WatchlistStatusResponse>, ApiError> {
|
||||
let on_watchlist = is_on_watchlist::execute(
|
||||
&state.app_ctx,
|
||||
IsOnWatchlistQuery { user_id: user.0.value(), movie_id },
|
||||
IsOnWatchlistQuery {
|
||||
user_id: user.0.value(),
|
||||
movie_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(WatchlistStatusResponse { on_watchlist }))
|
||||
|
||||
@@ -9,6 +9,25 @@ use axum::{
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
commands::{
|
||||
AddToWatchlistCommand, DeleteReviewCommand, MovieInput, RegisterCommand,
|
||||
RemoveFromWatchlistCommand,
|
||||
},
|
||||
ports::{
|
||||
HtmlPageContext, LoginPageData, MovieDetailPageData, NewReviewPageData,
|
||||
ProfileSettingsPageData, RegisterPageData, RemoteActorView, WatchlistDisplayEntry,
|
||||
WatchlistPageData,
|
||||
},
|
||||
queries::{
|
||||
ExportQuery, GetMovieSocialPageQuery, GetWatchlistQuery, IsOnWatchlistQuery, LoginQuery,
|
||||
},
|
||||
use_cases::{
|
||||
add_to_watchlist, delete_review, export_diary as export_diary_uc, get_movie_social_page,
|
||||
get_watchlist, is_on_watchlist, log_review, login as login_uc, register as register_uc,
|
||||
remove_from_watchlist, update_profile, update_profile_fields,
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use application::{
|
||||
ports::{
|
||||
@@ -17,29 +36,17 @@ use application::{
|
||||
},
|
||||
use_cases::get_remote_watchlist,
|
||||
};
|
||||
use application::{
|
||||
commands::{AddToWatchlistCommand, DeleteReviewCommand, MovieInput, RegisterCommand, RemoveFromWatchlistCommand},
|
||||
queries::{ExportQuery, GetMovieSocialPageQuery, GetWatchlistQuery, IsOnWatchlistQuery, LoginQuery},
|
||||
ports::{
|
||||
HtmlPageContext, LoginPageData, MovieDetailPageData, NewReviewPageData,
|
||||
ProfileSettingsPageData, RegisterPageData, RemoteActorView, WatchlistDisplayEntry,
|
||||
WatchlistPageData,
|
||||
},
|
||||
use_cases::{
|
||||
add_to_watchlist, delete_review, export_diary as export_diary_uc, get_movie_social_page,
|
||||
get_watchlist, is_on_watchlist, log_review, login as login_uc, register as register_uc,
|
||||
remove_from_watchlist, update_profile, update_profile_fields,
|
||||
},
|
||||
};
|
||||
use domain::models::ExportFormat;
|
||||
use domain::{errors::DomainError, value_objects::UserId};
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
use crate::forms::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm};
|
||||
use crate::forms::{
|
||||
ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm,
|
||||
};
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
forms::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm},
|
||||
extractors::{AdminUser, OptionalCookieUser, RequiredCookieUser},
|
||||
forms::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -58,7 +65,10 @@ pub(crate) async fn build_page_context(
|
||||
.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);
|
||||
let admin = user
|
||||
.as_ref()
|
||||
.map(|u| matches!(u.role(), domain::models::UserRole::Admin))
|
||||
.unwrap_or(false);
|
||||
(email, admin)
|
||||
} else {
|
||||
(None, false)
|
||||
@@ -219,18 +229,17 @@ pub async fn post_register(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
match login_uc::execute(&state.app_ctx, LoginQuery { email, password }).await {
|
||||
Ok(result) => {
|
||||
let max_age = (result.expires_at - Utc::now()).num_seconds().max(0);
|
||||
let cookie = set_cookie_header(&result.token, max_age);
|
||||
([cookie], Redirect::to("/")).into_response()
|
||||
}
|
||||
Err(_) => Redirect::to("/login").into_response(),
|
||||
Ok(_) => match login_uc::execute(&state.app_ctx, LoginQuery { email, password }).await {
|
||||
Ok(result) => {
|
||||
let max_age = (result.expires_at - Utc::now()).num_seconds().max(0);
|
||||
let cookie = set_cookie_header(&result.token, max_age);
|
||||
([cookie], Redirect::to("/")).into_response()
|
||||
}
|
||||
Err(_) => Redirect::to("/login").into_response(),
|
||||
},
|
||||
Err(_) => {
|
||||
Redirect::to("/register?error=Registration+failed.+Please+try+again.").into_response()
|
||||
}
|
||||
Err(_) => Redirect::to("/register?error=Registration+failed.+Please+try+again.")
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,10 +398,11 @@ pub async fn get_activity_feed(
|
||||
let mut remote_urls = Vec::new();
|
||||
for url in urls {
|
||||
if let Some(suffix) = url.strip_prefix(&format!("{}/users/", base_url))
|
||||
&& let Ok(parsed_id) = uuid::Uuid::parse_str(suffix) {
|
||||
local_ids.push(parsed_id);
|
||||
continue;
|
||||
}
|
||||
&& let Ok(parsed_id) = uuid::Uuid::parse_str(suffix)
|
||||
{
|
||||
local_ids.push(parsed_id);
|
||||
continue;
|
||||
}
|
||||
remote_urls.push(url);
|
||||
}
|
||||
Some(domain::ports::FollowingFilter {
|
||||
@@ -533,9 +543,7 @@ pub async fn get_user_profile(
|
||||
.get(axum::http::header::ACCEPT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
if accept.contains("application/activity+json")
|
||||
|| accept.contains("application/ld+json")
|
||||
{
|
||||
if accept.contains("application/activity+json") || accept.contains("application/ld+json") {
|
||||
return match state
|
||||
.ap_service
|
||||
.actor_json(&profile_user_uuid.to_string())
|
||||
@@ -667,8 +675,7 @@ pub async fn get_user_profile(
|
||||
.entries
|
||||
.as_ref()
|
||||
.map(|e| {
|
||||
let has_more =
|
||||
(e.offset as u64).saturating_add(e.limit as u64) < e.total_count;
|
||||
let has_more = (e.offset as u64).saturating_add(e.limit as u64) < e.total_count;
|
||||
(e.offset, has_more, e.limit)
|
||||
})
|
||||
.unwrap_or((0, false, super::DEFAULT_PAGE_LIMIT));
|
||||
@@ -730,7 +737,11 @@ pub async fn follow_remote_user(
|
||||
Err(e) => {
|
||||
tracing::error!("follow error: {:?}", e);
|
||||
let msg = encode_error(&e.to_string());
|
||||
let sep = if redirect_base.contains('?') { '&' } else { '?' };
|
||||
let sep = if redirect_base.contains('?') {
|
||||
'&'
|
||||
} else {
|
||||
'?'
|
||||
};
|
||||
Redirect::to(&format!("{}{}error={}", redirect_base, sep, msg)).into_response()
|
||||
}
|
||||
}
|
||||
@@ -755,8 +766,9 @@ pub async fn unfollow_remote_user(
|
||||
.unfollow(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to(&format!("/users/{}/following-list", profile_user_uuid))
|
||||
.into_response(),
|
||||
Ok(()) => {
|
||||
Redirect::to(&format!("/users/{}/following-list", profile_user_uuid)).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!(
|
||||
@@ -945,8 +957,9 @@ pub async fn remove_follower(
|
||||
.remove_follower(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Redirect::to(&format!("/users/{}/followers-list", profile_user_uuid))
|
||||
.into_response(),
|
||||
Ok(_) => {
|
||||
Redirect::to(&format!("/users/{}/followers-list", profile_user_uuid)).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!(
|
||||
@@ -971,7 +984,11 @@ pub async fn get_movie_detail(
|
||||
|
||||
match get_movie_social_page::execute(
|
||||
&state.app_ctx,
|
||||
GetMovieSocialPageQuery { movie_id, limit, offset },
|
||||
GetMovieSocialPageQuery {
|
||||
movie_id,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -982,13 +999,22 @@ pub async fn get_movie_detail(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
Ok(result) => {
|
||||
let histogram_max = result.stats.rating_histogram.iter().copied().max().unwrap_or(1);
|
||||
let has_more = result.reviews.offset + result.reviews.limit
|
||||
< result.reviews.total_count as u32;
|
||||
let histogram_max = result
|
||||
.stats
|
||||
.rating_histogram
|
||||
.iter()
|
||||
.copied()
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
let has_more =
|
||||
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,
|
||||
IsOnWatchlistQuery { user_id: uid.value(), movie_id },
|
||||
IsOnWatchlistQuery {
|
||||
user_id: uid.value(),
|
||||
movie_id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false),
|
||||
@@ -1030,7 +1056,9 @@ pub async fn get_watchlist_page(
|
||||
let is_owner = viewer_id.map(|u| u.value() == owner_id).unwrap_or(false);
|
||||
|
||||
// Try local user first
|
||||
let local_user = state.app_ctx.user_repository
|
||||
let local_user = state
|
||||
.app_ctx
|
||||
.user_repository
|
||||
.find_by_id(&domain::value_objects::UserId::from_uuid(owner_id))
|
||||
.await
|
||||
.ok()
|
||||
@@ -1039,30 +1067,42 @@ pub async fn get_watchlist_page(
|
||||
let (display_entries, has_more, current_offset, page_limit) = if local_user.is_some() {
|
||||
match get_watchlist::execute(
|
||||
&state.app_ctx,
|
||||
GetWatchlistQuery { user_id: owner_id, limit: Some(limit), offset: Some(offset) },
|
||||
).await {
|
||||
GetWatchlistQuery {
|
||||
user_id: owner_id,
|
||||
limit: Some(limit),
|
||||
offset: Some(offset),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
tracing::error!("watchlist error: {:?}", e);
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
Ok(entries) => {
|
||||
let has_more = entries.offset + entries.limit < entries.total_count as u32;
|
||||
let display: Vec<WatchlistDisplayEntry> = entries.items.iter().map(|w| {
|
||||
let remove_url = if is_owner {
|
||||
Some(format!("/watchlist/{}/remove", w.movie.id().value()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
WatchlistDisplayEntry {
|
||||
poster_url: w.movie.poster_path()
|
||||
.map(|p| format!("/images/{}", p.value())),
|
||||
movie_title: w.movie.title().value().to_string(),
|
||||
release_year: w.movie.release_year().value(),
|
||||
movie_url: Some(format!("/movies/{}", w.movie.id().value())),
|
||||
added_at: w.entry.added_at.format("%b %-d, %Y").to_string(),
|
||||
remove_url,
|
||||
}
|
||||
}).collect();
|
||||
let display: Vec<WatchlistDisplayEntry> = entries
|
||||
.items
|
||||
.iter()
|
||||
.map(|w| {
|
||||
let remove_url = if is_owner {
|
||||
Some(format!("/watchlist/{}/remove", w.movie.id().value()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
WatchlistDisplayEntry {
|
||||
poster_url: w
|
||||
.movie
|
||||
.poster_path()
|
||||
.map(|p| format!("/images/{}", p.value())),
|
||||
movie_title: w.movie.title().value().to_string(),
|
||||
release_year: w.movie.release_year().value(),
|
||||
movie_url: Some(format!("/movies/{}", w.movie.id().value())),
|
||||
added_at: w.entry.added_at.format("%b %-d, %Y").to_string(),
|
||||
remove_url,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(display, has_more, entries.offset, entries.limit)
|
||||
}
|
||||
}
|
||||
@@ -1072,16 +1112,17 @@ pub async fn get_watchlist_page(
|
||||
let remote_entries = get_remote_watchlist::execute(&state.app_ctx, owner_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let display: Vec<WatchlistDisplayEntry> = remote_entries.into_iter().map(|e| {
|
||||
WatchlistDisplayEntry {
|
||||
let display: Vec<WatchlistDisplayEntry> = remote_entries
|
||||
.into_iter()
|
||||
.map(|e| WatchlistDisplayEntry {
|
||||
poster_url: e.poster_url,
|
||||
movie_title: e.movie_title,
|
||||
release_year: e.release_year,
|
||||
movie_url: None,
|
||||
added_at: e.added_at.format("%b %-d, %Y").to_string(),
|
||||
remove_url: None,
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
let len = display.len() as u32;
|
||||
(display, false, 0u32, len)
|
||||
}
|
||||
@@ -1172,7 +1213,11 @@ pub async fn post_watchlist_add(
|
||||
Ok(()) => Redirect::to(&redirect_base).into_response(),
|
||||
Err(DomainError::NotFound(_)) => Redirect::to(&redirect_base).into_response(),
|
||||
Err(DomainError::ValidationError(msg)) => {
|
||||
let sep = if redirect_base.contains('?') { '&' } else { '?' };
|
||||
let sep = if redirect_base.contains('?') {
|
||||
'&'
|
||||
} else {
|
||||
'?'
|
||||
};
|
||||
let url = format!("{}{}error={}", redirect_base, sep, encode_error(&msg));
|
||||
Redirect::to(&url).into_response()
|
||||
}
|
||||
@@ -1231,12 +1276,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.user_repository.find_by_id(&user_id).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
@@ -1253,7 +1293,9 @@ pub async fn get_profile_settings(
|
||||
.banner_path()
|
||||
.map(|path| format!("{}/images/{}", base_url, path));
|
||||
|
||||
let profile_fields = state.app_ctx.profile_fields_repository
|
||||
let profile_fields = state
|
||||
.app_ctx
|
||||
.profile_fields_repository
|
||||
.get_fields(&user_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
@@ -1319,7 +1361,11 @@ pub async fn get_blocked_domains_page(
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("get_blocked_domains error: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to load blocked domains").into_response()
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to load blocked domains",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1335,7 +1381,11 @@ pub async fn post_blocked_domain(
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let reason = form.reason.as_deref().filter(|s| !s.trim().is_empty());
|
||||
match state.ap_service.add_blocked_domain(&form.domain, reason).await {
|
||||
match state
|
||||
.ap_service
|
||||
.add_blocked_domain(&form.domain, reason)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to("/admin/blocked-domains").into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("add_blocked_domain error: {:?}", e);
|
||||
@@ -1393,7 +1443,11 @@ pub async fn get_blocked_actors_page(
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("get_blocked_actors error: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to load blocked users").into_response()
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to load blocked users",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1408,7 +1462,11 @@ pub async fn post_block_actor_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state.ap_service.block_actor(user_id.value(), &form.actor_url).await {
|
||||
match state
|
||||
.ap_service
|
||||
.block_actor(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to("/social/blocked").into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("block_actor html error: {:?}", e);
|
||||
@@ -1427,7 +1485,11 @@ pub async fn post_unblock_actor(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match state.ap_service.unblock_actor(user_id.value(), &form.actor_url).await {
|
||||
match state
|
||||
.ap_service
|
||||
.unblock_actor(user_id.value(), &form.actor_url)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Redirect::to("/social/blocked").into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("unblock_actor error: {:?}", e);
|
||||
@@ -1447,13 +1509,19 @@ pub async fn post_profile_settings(
|
||||
let mut banner_bytes: Option<Vec<u8>> = None;
|
||||
let mut banner_content_type: Option<String> = None;
|
||||
let mut also_known_as: Option<String> = None;
|
||||
let mut field_names: std::collections::HashMap<usize, String> = std::collections::HashMap::new();
|
||||
let mut field_values: std::collections::HashMap<usize, String> = std::collections::HashMap::new();
|
||||
let mut field_names: std::collections::HashMap<usize, String> =
|
||||
std::collections::HashMap::new();
|
||||
let mut field_values: std::collections::HashMap<usize, String> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
while let Ok(Some(field)) = multipart.next_field().await {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
match name.as_str() {
|
||||
"bio" => { if let Ok(text) = field.text().await { bio = Some(text); } }
|
||||
"bio" => {
|
||||
if let Ok(text) = field.text().await {
|
||||
bio = Some(text);
|
||||
}
|
||||
}
|
||||
"also_known_as" => {
|
||||
if let Ok(text) = field.text().await {
|
||||
also_known_as = Some(text).filter(|s| !s.is_empty());
|
||||
@@ -1462,26 +1530,36 @@ pub async fn post_profile_settings(
|
||||
"avatar" => {
|
||||
let ct = field.content_type().map(|s| s.to_string());
|
||||
if let Ok(bytes) = field.bytes().await {
|
||||
if !bytes.is_empty() { avatar_bytes = Some(bytes.to_vec()); avatar_content_type = ct; }
|
||||
if !bytes.is_empty() {
|
||||
avatar_bytes = Some(bytes.to_vec());
|
||||
avatar_content_type = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
"banner" => {
|
||||
let ct = field.content_type().map(|s| s.to_string());
|
||||
if let Ok(bytes) = field.bytes().await {
|
||||
if !bytes.is_empty() { banner_bytes = Some(bytes.to_vec()); banner_content_type = ct; }
|
||||
if !bytes.is_empty() {
|
||||
banner_bytes = Some(bytes.to_vec());
|
||||
banner_content_type = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
n if n.starts_with("field_name_") => {
|
||||
if let Ok(idx) = n["field_name_".len()..].parse::<usize>() {
|
||||
if let Ok(text) = field.text().await {
|
||||
if !text.is_empty() { field_names.insert(idx, text); }
|
||||
if !text.is_empty() {
|
||||
field_names.insert(idx, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
n if n.starts_with("field_value_") => {
|
||||
if let Ok(idx) = n["field_value_".len()..].parse::<usize>() {
|
||||
if let Ok(text) = field.text().await {
|
||||
if !text.is_empty() { field_values.insert(idx, text); }
|
||||
if !text.is_empty() {
|
||||
field_values.insert(idx, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1502,10 +1580,12 @@ pub async fn post_profile_settings(
|
||||
|
||||
let fields: Vec<domain::models::ProfileField> = (0..4)
|
||||
.filter_map(|i| {
|
||||
field_names.get(&i).map(|name| domain::models::ProfileField {
|
||||
name: name.clone(),
|
||||
value: field_values.get(&i).cloned().unwrap_or_default(),
|
||||
})
|
||||
field_names
|
||||
.get(&i)
|
||||
.map(|name| domain::models::ProfileField {
|
||||
name: name.clone(),
|
||||
value: field_values.get(&i).cloned().unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use api_types::{
|
||||
ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, SessionCreatedResponse,
|
||||
SessionStateResponse,
|
||||
};
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::{Multipart, Path, State},
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Redirect},
|
||||
};
|
||||
use api_types::{
|
||||
ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, SessionCreatedResponse,
|
||||
SessionStateResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -25,7 +25,10 @@ use application::{
|
||||
list_import_profiles, save_import_profile,
|
||||
},
|
||||
};
|
||||
use domain::models::{AnnotatedRow, FieldMapping, FileFormat, import::{DomainField, RowResult, Transform}};
|
||||
use domain::models::{
|
||||
AnnotatedRow, FieldMapping, FileFormat,
|
||||
import::{DomainField, RowResult, Transform},
|
||||
};
|
||||
use domain::value_objects::ImportSessionId;
|
||||
|
||||
use crate::{
|
||||
@@ -196,11 +199,10 @@ pub async fn post_upload(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
Redirect::to(&format!("/import/{}/mapping", r.session_id.value())).into_response()
|
||||
Ok(r) => Redirect::to(&format!("/import/{}/mapping", r.session_id.value())).into_response(),
|
||||
Err(e) => {
|
||||
Redirect::to(&format!("/import?error={}", encode_error(&e.to_string()))).into_response()
|
||||
}
|
||||
Err(e) => Redirect::to(&format!("/import?error={}", encode_error(&e.to_string())))
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,8 +410,9 @@ pub async fn post_confirm(
|
||||
summary.failed.len()
|
||||
))
|
||||
.into_response(),
|
||||
Err(e) => Redirect::to(&format!("/import?error={}", encode_error(&e.to_string())))
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
Redirect::to(&format!("/import?error={}", encode_error(&e.to_string()))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod api;
|
||||
pub mod html;
|
||||
pub mod images;
|
||||
pub mod rss;
|
||||
pub mod api;
|
||||
pub mod import;
|
||||
pub mod rss;
|
||||
|
||||
const DEFAULT_PAGE_LIMIT: u32 = 5;
|
||||
const RSS_FEED_LIMIT: u32 = 50;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod csrf;
|
||||
pub mod forms;
|
||||
pub mod errors;
|
||||
pub mod extractors;
|
||||
pub mod forms;
|
||||
pub mod handlers;
|
||||
pub mod openapi;
|
||||
pub mod ports;
|
||||
|
||||
@@ -13,13 +13,17 @@ use template_askama::AskamaHtmlRenderer;
|
||||
|
||||
use presentation::{openapi, routes, state::AppState};
|
||||
|
||||
use domain::ports::{DiaryExporter, DocumentParser, EventPublisher, ImportProfileRepository, ImportSessionRepository};
|
||||
use domain::ports::{
|
||||
DiaryExporter, DocumentParser, EventPublisher, ImportProfileRepository, ImportSessionRepository,
|
||||
};
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use postgres_search;
|
||||
|
||||
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||
compile_error!("At least one database backend must be enabled. Use --features sqlite or --features postgres");
|
||||
compile_error!(
|
||||
"At least one database backend must be enabled. Use --features sqlite or --features postgres"
|
||||
);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
@@ -37,7 +41,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!("Listening on {}", addr);
|
||||
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()).await?;
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -52,25 +60,71 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
let poster_fetcher = poster_fetcher::create()?;
|
||||
let image_storage = image_storage::create()?;
|
||||
|
||||
let (movie_repository, review_repository, diary_repository, stats_repository, user_repository, import_session_repository, import_profile_repository, movie_profile_repository, watchlist_repository, person_command, person_query, search_command, search_port, db_pool) =
|
||||
match backend.as_str() {
|
||||
#[cfg(feature = "postgres")]
|
||||
"postgres" => {
|
||||
let (pool, m, r, d, s, u, is, ip, mp, wl) = postgres::wire(&database_url).await?;
|
||||
let (pc, pq) = postgres::create_person_adapter(pool.clone());
|
||||
let (sc, sp) = postgres_search::create_search_adapter(pool.clone());
|
||||
(m, r, d, s, u, is, ip, mp, wl, pc, pq, sc, sp, DbPool::Postgres(pool))
|
||||
}
|
||||
#[cfg(feature = "sqlite")]
|
||||
_ => {
|
||||
let (pool, m, r, d, s, u, is, ip, mp, wl) = sqlite::wire(&database_url).await?;
|
||||
let (pc, pq) = sqlite::create_person_adapter(pool.clone());
|
||||
let (sc, sp) = sqlite_search::create_search_adapter(pool.clone());
|
||||
(m, r, d, s, u, is, ip, mp, wl, pc, pq, sc, sp, DbPool::Sqlite(pool))
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
_ => anyhow::bail!("DATABASE_BACKEND={backend} is not supported by this build (sqlite feature is not enabled)"),
|
||||
};
|
||||
let (
|
||||
movie_repository,
|
||||
review_repository,
|
||||
diary_repository,
|
||||
stats_repository,
|
||||
user_repository,
|
||||
import_session_repository,
|
||||
import_profile_repository,
|
||||
movie_profile_repository,
|
||||
watchlist_repository,
|
||||
person_command,
|
||||
person_query,
|
||||
search_command,
|
||||
search_port,
|
||||
db_pool,
|
||||
) = match backend.as_str() {
|
||||
#[cfg(feature = "postgres")]
|
||||
"postgres" => {
|
||||
let (pool, m, r, d, s, u, is, ip, mp, wl) = postgres::wire(&database_url).await?;
|
||||
let (pc, pq) = postgres::create_person_adapter(pool.clone());
|
||||
let (sc, sp) = postgres_search::create_search_adapter(pool.clone());
|
||||
(
|
||||
m,
|
||||
r,
|
||||
d,
|
||||
s,
|
||||
u,
|
||||
is,
|
||||
ip,
|
||||
mp,
|
||||
wl,
|
||||
pc,
|
||||
pq,
|
||||
sc,
|
||||
sp,
|
||||
DbPool::Postgres(pool),
|
||||
)
|
||||
}
|
||||
#[cfg(feature = "sqlite")]
|
||||
_ => {
|
||||
let (pool, m, r, d, s, u, is, ip, mp, wl) = sqlite::wire(&database_url).await?;
|
||||
let (pc, pq) = sqlite::create_person_adapter(pool.clone());
|
||||
let (sc, sp) = sqlite_search::create_search_adapter(pool.clone());
|
||||
(
|
||||
m,
|
||||
r,
|
||||
d,
|
||||
s,
|
||||
u,
|
||||
is,
|
||||
ip,
|
||||
mp,
|
||||
wl,
|
||||
pc,
|
||||
pq,
|
||||
sc,
|
||||
sp,
|
||||
DbPool::Sqlite(pool),
|
||||
)
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
_ => anyhow::bail!(
|
||||
"DATABASE_BACKEND={backend} is not supported by this build (sqlite feature is not enabled)"
|
||||
),
|
||||
};
|
||||
|
||||
let profile_fields_repo = match &db_pool {
|
||||
#[cfg(feature = "postgres")]
|
||||
@@ -86,27 +140,31 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
let (event_publisher_arc, ap_router, ap_service, social_query, remote_watchlist_repo) = {
|
||||
let (federation_repo, social_query_arc, review_store, remote_watchlist_repo) = match &db_pool {
|
||||
#[cfg(feature = "postgres-federation")]
|
||||
DbPool::Postgres(pool) => postgres_federation::wire(pool.clone()),
|
||||
#[cfg(feature = "sqlite-federation")]
|
||||
DbPool::Sqlite(pool) => sqlite_federation::wire(pool.clone()),
|
||||
#[cfg(not(feature = "sqlite-federation"))]
|
||||
_ => anyhow::bail!("DATABASE_BACKEND={backend} federation is not supported by this build"),
|
||||
};
|
||||
let (federation_repo, social_query_arc, review_store, remote_watchlist_repo) =
|
||||
match &db_pool {
|
||||
#[cfg(feature = "postgres-federation")]
|
||||
DbPool::Postgres(pool) => postgres_federation::wire(pool.clone()),
|
||||
#[cfg(feature = "sqlite-federation")]
|
||||
DbPool::Sqlite(pool) => sqlite_federation::wire(pool.clone()),
|
||||
#[cfg(not(feature = "sqlite-federation"))]
|
||||
_ => anyhow::bail!(
|
||||
"DATABASE_BACKEND={backend} federation is not supported by this build"
|
||||
),
|
||||
};
|
||||
|
||||
let ep: Arc<dyn EventPublisher> = match event_bus {
|
||||
EventBusBackend::Db => {
|
||||
tracing::info!("event bus: DB queue");
|
||||
match &db_pool {
|
||||
#[cfg(feature = "postgres")]
|
||||
DbPool::Postgres(pool) => postgres_event_queue::PostgresEventQueue::create_publisher(
|
||||
pool.clone()
|
||||
).await?,
|
||||
DbPool::Postgres(pool) => {
|
||||
postgres_event_queue::PostgresEventQueue::create_publisher(pool.clone())
|
||||
.await?
|
||||
}
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbPool::Sqlite(pool) => sqlite_event_queue::SqliteEventQueue::create_publisher(
|
||||
pool.clone()
|
||||
).await?,
|
||||
DbPool::Sqlite(pool) => {
|
||||
sqlite_event_queue::SqliteEventQueue::create_publisher(pool.clone()).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "nats")]
|
||||
@@ -129,11 +187,18 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
app_config.base_url.clone(),
|
||||
app_config.allow_registration,
|
||||
Arc::clone(&ep),
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
let ap_router = ap.router;
|
||||
let ap_service_arc = ap.service;
|
||||
|
||||
(ep, ap_router, ap_service_arc, social_query_arc, remote_watchlist_repo)
|
||||
(
|
||||
ep,
|
||||
ap_router,
|
||||
ap_service_arc,
|
||||
social_query_arc,
|
||||
remote_watchlist_repo,
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "federation"))]
|
||||
@@ -142,15 +207,17 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
||||
tracing::info!("event bus: DB queue");
|
||||
match &db_pool {
|
||||
#[cfg(feature = "postgres")]
|
||||
DbPool::Postgres(pool) => postgres_event_queue::PostgresEventQueue::create_publisher(
|
||||
pool.clone()
|
||||
).await?,
|
||||
DbPool::Postgres(pool) => {
|
||||
postgres_event_queue::PostgresEventQueue::create_publisher(pool.clone()).await?
|
||||
}
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbPool::Sqlite(pool) => sqlite_event_queue::SqliteEventQueue::create_publisher(
|
||||
pool.clone()
|
||||
).await?,
|
||||
DbPool::Sqlite(pool) => {
|
||||
sqlite_event_queue::SqliteEventQueue::create_publisher(pool.clone()).await?
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
_ => anyhow::bail!("EVENT_BUS_BACKEND=db has no adapter for DATABASE_BACKEND={backend}; enable the sqlite or postgres feature"),
|
||||
_ => anyhow::bail!(
|
||||
"EVENT_BUS_BACKEND=db has no adapter for DATABASE_BACKEND={backend}; enable the sqlite or postgres feature"
|
||||
),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "nats")]
|
||||
@@ -213,7 +280,6 @@ enum DbPool {
|
||||
Postgres(sqlx::PgPool),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum EventBusBackend {
|
||||
Db,
|
||||
@@ -231,7 +297,9 @@ impl EventBusBackend {
|
||||
#[cfg(feature = "nats")]
|
||||
"nats" => Ok(Self::Nats),
|
||||
#[cfg(not(feature = "nats"))]
|
||||
"nats" => anyhow::bail!("EVENT_BUS_BACKEND=nats requires the nats feature to be compiled in"),
|
||||
"nats" => {
|
||||
anyhow::bail!("EVENT_BUS_BACKEND=nats requires the nats feature to be compiled in")
|
||||
}
|
||||
other => anyhow::bail!("unknown EVENT_BUS_BACKEND={other}, expected 'db' or 'nats'"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::handlers::api::login,
|
||||
crate::handlers::api::register,
|
||||
),
|
||||
components(schemas(LoginRequest, LoginResponse, RegisterRequest)),
|
||||
paths(crate::handlers::api::login, crate::handlers::api::register,),
|
||||
components(schemas(LoginRequest, LoginResponse, RegisterRequest))
|
||||
)]
|
||||
pub struct AuthDoc;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use api_types::{ActivityFeedResponse, DiaryEntryDto, DiaryResponse, FeedEntryDto, LogReviewRequest, ReviewDto};
|
||||
use api_types::{
|
||||
ActivityFeedResponse, DiaryEntryDto, DiaryResponse, FeedEntryDto, LogReviewRequest, ReviewDto,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
@@ -17,6 +19,6 @@ use utoipa::OpenApi;
|
||||
LogReviewRequest,
|
||||
ActivityFeedResponse,
|
||||
FeedEntryDto,
|
||||
)),
|
||||
))
|
||||
)]
|
||||
pub struct DiaryDoc;
|
||||
|
||||
@@ -22,6 +22,6 @@ use utoipa::OpenApi;
|
||||
ApplyMappingRequest,
|
||||
ConfirmRequest,
|
||||
SaveProfileRequest,
|
||||
)),
|
||||
))
|
||||
)]
|
||||
pub struct ImportDoc;
|
||||
|
||||
@@ -33,6 +33,6 @@ use utoipa::OpenApi;
|
||||
BlockedDomainResponse,
|
||||
AddBlockedDomainRequest,
|
||||
BlockedActorResponse,
|
||||
)),
|
||||
))
|
||||
)]
|
||||
pub struct SocialDoc;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use api_types::{ProfileResponse, UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse};
|
||||
use api_types::{
|
||||
ProfileResponse, UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
@@ -15,6 +17,6 @@ use utoipa::OpenApi;
|
||||
UserProfileResponse,
|
||||
UserStatsDto,
|
||||
ProfileResponse,
|
||||
)),
|
||||
))
|
||||
)]
|
||||
pub struct UsersDoc;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use api_types::{AddToWatchlistRequest, WatchlistEntryDto, WatchlistResponse, WatchlistStatusResponse};
|
||||
use api_types::{
|
||||
AddToWatchlistRequest, WatchlistEntryDto, WatchlistResponse, WatchlistStatusResponse,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
|
||||
@@ -60,7 +60,10 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||
let base = Router::new()
|
||||
.route("/", routing::get(handlers::html::get_activity_feed))
|
||||
.route("/users", routing::get(handlers::html::get_users_list))
|
||||
.route("/u/{username}", routing::get(handlers::html::get_user_by_username))
|
||||
.route(
|
||||
"/u/{username}",
|
||||
routing::get(handlers::html::get_user_by_username),
|
||||
)
|
||||
.route(
|
||||
"/users/{id}",
|
||||
routing::get(handlers::html::get_user_profile),
|
||||
@@ -79,24 +82,41 @@ fn html_routes(rate_limit: u64) -> Router<AppState> {
|
||||
"/reviews/{id}/delete",
|
||||
routing::post(handlers::html::post_delete_review),
|
||||
)
|
||||
.route(
|
||||
"/images/{*key}",
|
||||
routing::get(handlers::images::get_image),
|
||||
)
|
||||
.route("/images/{*key}", routing::get(handlers::images::get_image))
|
||||
.route(
|
||||
"/posters/{path}",
|
||||
routing::get(|axum::extract::Path(p): axum::extract::Path<String>| async move {
|
||||
axum::response::Redirect::permanent(&format!("/images/{}", p))
|
||||
}),
|
||||
routing::get(
|
||||
|axum::extract::Path(p): axum::extract::Path<String>| async move {
|
||||
axum::response::Redirect::permanent(&format!("/images/{}", p))
|
||||
},
|
||||
),
|
||||
)
|
||||
.route("/diary/export", routing::get(handlers::html::get_export))
|
||||
.route("/import", routing::get(handlers::import::get_import_page))
|
||||
.route("/import/upload", routing::post(handlers::import::post_upload))
|
||||
.route("/import/{id}/mapping", routing::get(handlers::import::get_mapping_page).post(handlers::import::post_mapping))
|
||||
.route("/import/{id}/preview", routing::get(handlers::import::get_preview_page))
|
||||
.route("/import/{id}/confirm", routing::post(handlers::import::post_confirm))
|
||||
.route("/import/done", routing::get(handlers::import::get_import_done))
|
||||
.route("/import/profiles/{profile_id}/delete", routing::post(handlers::import::post_delete_profile))
|
||||
.route(
|
||||
"/import/upload",
|
||||
routing::post(handlers::import::post_upload),
|
||||
)
|
||||
.route(
|
||||
"/import/{id}/mapping",
|
||||
routing::get(handlers::import::get_mapping_page).post(handlers::import::post_mapping),
|
||||
)
|
||||
.route(
|
||||
"/import/{id}/preview",
|
||||
routing::get(handlers::import::get_preview_page),
|
||||
)
|
||||
.route(
|
||||
"/import/{id}/confirm",
|
||||
routing::post(handlers::import::post_confirm),
|
||||
)
|
||||
.route(
|
||||
"/import/done",
|
||||
routing::get(handlers::import::get_import_done),
|
||||
)
|
||||
.route(
|
||||
"/import/profiles/{profile_id}/delete",
|
||||
routing::post(handlers::import::post_delete_profile),
|
||||
)
|
||||
.route("/feed.rss", routing::get(handlers::rss::get_feed))
|
||||
.route(
|
||||
"/users/{id}/feed.rss",
|
||||
@@ -171,8 +191,14 @@ fn federation_html_routes() -> Router<AppState> {
|
||||
"/social/blocked",
|
||||
routing::get(handlers::html::get_blocked_actors_page),
|
||||
)
|
||||
.route("/social/block", routing::post(handlers::html::post_block_actor_html))
|
||||
.route("/social/unblock", routing::post(handlers::html::post_unblock_actor))
|
||||
.route(
|
||||
"/social/block",
|
||||
routing::post(handlers::html::post_block_actor_html),
|
||||
)
|
||||
.route(
|
||||
"/social/unblock",
|
||||
routing::post(handlers::html::post_unblock_actor),
|
||||
)
|
||||
}
|
||||
|
||||
fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||
@@ -216,17 +242,48 @@ fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||
)
|
||||
.route("/users", routing::get(handlers::api::list_users))
|
||||
.route("/users/{id}", routing::get(handlers::api::get_user_profile))
|
||||
.route("/import/sessions", routing::post(handlers::import::api_post_session))
|
||||
.route("/import/sessions/{id}", routing::get(handlers::import::api_get_session))
|
||||
.route("/import/sessions/{id}/mapping", routing::put(handlers::import::api_put_mapping))
|
||||
.route("/import/sessions/{id}/confirm", routing::post(handlers::import::api_post_confirm))
|
||||
.route("/import/profiles", routing::get(handlers::import::api_get_profiles).post(handlers::import::api_post_profile))
|
||||
.route("/import/profiles/{id}", routing::delete(handlers::import::api_delete_profile))
|
||||
.route("/profile", routing::get(handlers::api::get_profile).put(handlers::api::update_profile_handler))
|
||||
.route("/profile/fields", routing::put(handlers::api::update_profile_fields_handler))
|
||||
.route(
|
||||
"/import/sessions",
|
||||
routing::post(handlers::import::api_post_session),
|
||||
)
|
||||
.route(
|
||||
"/import/sessions/{id}",
|
||||
routing::get(handlers::import::api_get_session),
|
||||
)
|
||||
.route(
|
||||
"/import/sessions/{id}/mapping",
|
||||
routing::put(handlers::import::api_put_mapping),
|
||||
)
|
||||
.route(
|
||||
"/import/sessions/{id}/confirm",
|
||||
routing::post(handlers::import::api_post_confirm),
|
||||
)
|
||||
.route(
|
||||
"/import/profiles",
|
||||
routing::get(handlers::import::api_get_profiles)
|
||||
.post(handlers::import::api_post_profile),
|
||||
)
|
||||
.route(
|
||||
"/import/profiles/{id}",
|
||||
routing::delete(handlers::import::api_delete_profile),
|
||||
)
|
||||
.route(
|
||||
"/profile",
|
||||
routing::get(handlers::api::get_profile).put(handlers::api::update_profile_handler),
|
||||
)
|
||||
.route(
|
||||
"/profile/fields",
|
||||
routing::put(handlers::api::update_profile_fields_handler),
|
||||
)
|
||||
.route("/search", routing::get(handlers::api::get_search))
|
||||
.route("/people/{id}", routing::get(handlers::api::get_person_handler))
|
||||
.route("/people/{id}/credits", routing::get(handlers::api::get_person_credits_handler))
|
||||
.route(
|
||||
"/people/{id}",
|
||||
routing::get(handlers::api::get_person_handler),
|
||||
)
|
||||
.route(
|
||||
"/people/{id}/credits",
|
||||
routing::get(handlers::api::get_person_credits_handler),
|
||||
)
|
||||
.route(
|
||||
"/watchlist",
|
||||
routing::get(handlers::api::get_watchlist_handler)
|
||||
@@ -282,7 +339,16 @@ fn federation_api_routes() -> Router<AppState> {
|
||||
"/admin/blocked-domains/{domain}",
|
||||
routing::delete(handlers::api::remove_blocked_domain_admin),
|
||||
)
|
||||
.route("/social/block", routing::post(handlers::api::block_actor_api))
|
||||
.route("/social/unblock", routing::post(handlers::api::unblock_actor_api))
|
||||
.route("/social/blocked", routing::get(handlers::api::get_blocked_actors_api))
|
||||
.route(
|
||||
"/social/block",
|
||||
routing::post(handlers::api::block_actor_api),
|
||||
)
|
||||
.route(
|
||||
"/social/unblock",
|
||||
routing::post(handlers::api::unblock_actor_api),
|
||||
)
|
||||
.route(
|
||||
"/social/blocked",
|
||||
routing::get(handlers::api::get_blocked_actors_api),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::extractors::{make_test_state, Panic};
|
||||
use super::extractors::{Panic, make_test_state};
|
||||
use axum::{
|
||||
Router,
|
||||
body::Body,
|
||||
@@ -14,7 +14,10 @@ use uuid::Uuid;
|
||||
struct SearchPortStub;
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::SearchPort for SearchPortStub {
|
||||
async fn search(&self, _: &domain::models::SearchQuery) -> Result<domain::models::SearchResults, DomainError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_: &domain::models::SearchQuery,
|
||||
) -> Result<domain::models::SearchResults, DomainError> {
|
||||
Ok(domain::models::SearchResults {
|
||||
movies: domain::models::collections::Paginated {
|
||||
items: vec![],
|
||||
@@ -36,13 +39,22 @@ impl domain::ports::SearchPort for SearchPortStub {
|
||||
struct PersonQueryStub;
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::PersonQuery for PersonQueryStub {
|
||||
async fn get_by_id(&self, _: &domain::models::PersonId) -> Result<Option<domain::models::Person>, DomainError> {
|
||||
Ok(None) // Return None to trigger 404
|
||||
async fn get_by_id(
|
||||
&self,
|
||||
_: &domain::models::PersonId,
|
||||
) -> Result<Option<domain::models::Person>, DomainError> {
|
||||
Ok(None) // Return None to trigger 404
|
||||
}
|
||||
async fn get_by_external_id(&self, _: &domain::models::ExternalPersonId) -> Result<Option<domain::models::Person>, DomainError> {
|
||||
async fn get_by_external_id(
|
||||
&self,
|
||||
_: &domain::models::ExternalPersonId,
|
||||
) -> Result<Option<domain::models::Person>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn get_credits(&self, _: &domain::models::PersonId) -> Result<domain::models::PersonCredits, DomainError> {
|
||||
async fn get_credits(
|
||||
&self,
|
||||
_: &domain::models::PersonId,
|
||||
) -> Result<domain::models::PersonCredits, DomainError> {
|
||||
Err(DomainError::NotFound("Person not found".into()))
|
||||
}
|
||||
async fn list_orphaned_persons(&self) -> Result<Vec<domain::models::PersonId>, DomainError> {
|
||||
@@ -104,7 +116,10 @@ async fn person_endpoint_returns_404_for_unknown_id() {
|
||||
// Override the person_query with our stub
|
||||
state.app_ctx.person_query = Arc::new(PersonQueryStub);
|
||||
let app = Router::new()
|
||||
.route("/api/v1/people/{id}", get(crate::handlers::api::get_person_handler))
|
||||
.route(
|
||||
"/api/v1/people/{id}",
|
||||
get(crate::handlers::api::get_person_handler),
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let unknown_id = Uuid::new_v4();
|
||||
@@ -127,7 +142,10 @@ async fn person_credits_endpoint_returns_404_for_unknown_id() {
|
||||
// Override the person_query with our stub
|
||||
state.app_ctx.person_query = Arc::new(PersonQueryStub);
|
||||
let app = Router::new()
|
||||
.route("/api/v1/people/{id}/credits", get(crate::handlers::api::get_person_credits_handler))
|
||||
.route(
|
||||
"/api/v1/people/{id}/credits",
|
||||
get(crate::handlers::api::get_person_credits_handler),
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let unknown_id = Uuid::new_v4();
|
||||
@@ -150,7 +168,10 @@ async fn person_credits_endpoint_returns_404_for_unknown_id() {
|
||||
async fn get_watchlist_requires_auth() {
|
||||
let state = make_test_state(Arc::new(Panic));
|
||||
let app = Router::new()
|
||||
.route("/api/v1/watchlist", get(crate::handlers::api::get_watchlist_handler))
|
||||
.route(
|
||||
"/api/v1/watchlist",
|
||||
get(crate::handlers::api::get_watchlist_handler),
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let resp = app
|
||||
|
||||
@@ -10,21 +10,20 @@ use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::{
|
||||
DiaryEntry, DiaryFilter, FeedEntry, Movie, Review, ReviewHistory, UserStats,
|
||||
DiaryEntry, DiaryFilter, EntityType, FeedEntry, IndexableDocument, Movie, Person,
|
||||
PersonCredits, PersonId, Review, ReviewHistory, SearchQuery, SearchResults, UserStats,
|
||||
UserTrends,
|
||||
collections::{PageParams, Paginated},
|
||||
PersonId, EntityType, IndexableDocument, Person, PersonCredits,
|
||||
SearchQuery, SearchResults,
|
||||
},
|
||||
ports::{
|
||||
AuthService, DiaryRepository, EventPublisher, GeneratedToken, ImageStorage,
|
||||
MetadataClient, MovieRepository, PasswordHasher, PosterFetcherClient, ReviewRepository,
|
||||
StatsRepository, UserRepository, WatchlistRepository,
|
||||
PersonCommand, PersonQuery, SearchPort, SearchCommand,
|
||||
AuthService, DiaryRepository, EventPublisher, GeneratedToken, ImageStorage, MetadataClient,
|
||||
MovieRepository, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
|
||||
ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserRepository,
|
||||
WatchlistRepository,
|
||||
},
|
||||
value_objects::{
|
||||
Email, ExternalMetadataId, MovieId, MovieTitle, PasswordHash, PosterUrl,
|
||||
ReleaseYear, ReviewId, UserId,
|
||||
Email, ExternalMetadataId, MovieId, MovieTitle, PasswordHash, PosterUrl, ReleaseYear,
|
||||
ReviewId, UserId,
|
||||
},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -58,7 +57,12 @@ impl MovieRepository for Panic {
|
||||
async fn delete_movie(&self, _: &MovieId) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_movies(&self, _: &domain::models::collections::PageParams, _: &domain::models::MovieFilter) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError> {
|
||||
async fn list_movies(
|
||||
&self,
|
||||
_: &domain::models::collections::PageParams,
|
||||
_: &domain::models::MovieFilter,
|
||||
) -> Result<domain::models::collections::Paginated<domain::models::MovieSummary>, DomainError>
|
||||
{
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
@@ -123,10 +127,7 @@ impl DiaryRepository for Panic {
|
||||
#[cfg(feature = "federation")]
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::SocialQueryPort for Panic {
|
||||
async fn get_accepted_following_urls(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
) -> Result<Vec<String>, DomainError> {
|
||||
async fn get_accepted_following_urls(&self, _: uuid::Uuid) -> Result<Vec<String>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_all_followed_remote_actors(
|
||||
@@ -167,9 +168,15 @@ impl PosterFetcherClient for Panic {
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl ImageStorage for Panic {
|
||||
async fn store(&self, _: &str, _: &[u8]) -> Result<String, DomainError> { panic!() }
|
||||
async fn get(&self, _: &str) -> Result<Vec<u8>, DomainError> { panic!() }
|
||||
async fn delete(&self, _: &str) -> Result<(), DomainError> { panic!() }
|
||||
async fn store(&self, _: &str, _: &[u8]) -> Result<String, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get(&self, _: &str) -> Result<Vec<u8>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete(&self, _: &str) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl AuthService for Panic {
|
||||
@@ -191,19 +198,13 @@ impl PasswordHasher for Panic {
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl UserRepository for Panic {
|
||||
async fn find_by_email(
|
||||
&self,
|
||||
_: &Email,
|
||||
) -> Result<Option<domain::models::User>, DomainError> {
|
||||
async fn find_by_email(&self, _: &Email) -> Result<Option<domain::models::User>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn save(&self, _: &domain::models::User) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn find_by_id(
|
||||
&self,
|
||||
_: &UserId,
|
||||
) -> Result<Option<domain::models::User>, DomainError> {
|
||||
async fn find_by_id(&self, _: &UserId) -> Result<Option<domain::models::User>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn find_by_username(
|
||||
@@ -215,14 +216,32 @@ impl UserRepository for Panic {
|
||||
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn update_profile(&self, _: &UserId, _: Option<String>, _: Option<String>, _: Option<String>, _: Option<String>) -> Result<(), DomainError> {
|
||||
async fn update_profile(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: Option<String>,
|
||||
_: Option<String>,
|
||||
_: Option<String>,
|
||||
_: Option<String>,
|
||||
) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::UserProfileFieldsRepository for Panic {
|
||||
async fn get_fields(&self, _: &UserId) -> Result<Vec<domain::models::ProfileField>, DomainError> { panic!() }
|
||||
async fn set_fields(&self, _: &UserId, _: Vec<domain::models::ProfileField>) -> Result<(), DomainError> { panic!() }
|
||||
async fn get_fields(
|
||||
&self,
|
||||
_: &UserId,
|
||||
) -> Result<Vec<domain::models::ProfileField>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn set_fields(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: Vec<domain::models::ProfileField>,
|
||||
) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl EventPublisher for Panic {
|
||||
@@ -232,33 +251,104 @@ impl EventPublisher for Panic {
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::ImportSessionRepository for Panic {
|
||||
async fn create(&self, _: &domain::models::ImportSession) -> Result<(), DomainError> { panic!() }
|
||||
async fn get(&self, _: &domain::value_objects::ImportSessionId, _: &UserId) -> Result<Option<domain::models::ImportSession>, DomainError> { panic!() }
|
||||
async fn update(&self, _: &domain::models::ImportSession) -> Result<(), DomainError> { panic!() }
|
||||
async fn delete(&self, _: &domain::value_objects::ImportSessionId) -> Result<(), DomainError> { panic!() }
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError> { panic!() }
|
||||
async fn delete_expired_for_user(&self, _: &UserId) -> Result<(), DomainError> { panic!() }
|
||||
async fn create(&self, _: &domain::models::ImportSession) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
_: &domain::value_objects::ImportSessionId,
|
||||
_: &UserId,
|
||||
) -> Result<Option<domain::models::ImportSession>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn update(&self, _: &domain::models::ImportSession) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete(&self, _: &domain::value_objects::ImportSessionId) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete_expired_for_user(&self, _: &UserId) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::ImportProfileRepository for Panic {
|
||||
async fn save(&self, _: &domain::models::ImportProfile) -> Result<(), DomainError> { panic!() }
|
||||
async fn list_for_user(&self, _: &UserId) -> Result<Vec<domain::models::ImportProfile>, DomainError> { panic!() }
|
||||
async fn get(&self, _: &domain::value_objects::ImportProfileId, _: &UserId) -> Result<Option<domain::models::ImportProfile>, DomainError> { panic!() }
|
||||
async fn delete(&self, _: &domain::value_objects::ImportProfileId) -> Result<(), DomainError> { panic!() }
|
||||
async fn save(&self, _: &domain::models::ImportProfile) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_for_user(
|
||||
&self,
|
||||
_: &UserId,
|
||||
) -> Result<Vec<domain::models::ImportProfile>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
_: &domain::value_objects::ImportProfileId,
|
||||
_: &UserId,
|
||||
) -> Result<Option<domain::models::ImportProfile>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn delete(&self, _: &domain::value_objects::ImportProfileId) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl WatchlistRepository for Panic {
|
||||
async fn add(&self, _: &domain::models::WatchlistEntry) -> Result<(), DomainError> { panic!() }
|
||||
async fn remove(&self, _: &domain::value_objects::UserId, _: &domain::value_objects::MovieId) -> Result<(), DomainError> { panic!() }
|
||||
async fn remove_if_present(&self, _: &domain::value_objects::UserId, _: &domain::value_objects::MovieId) -> Result<bool, DomainError> { Ok(false) }
|
||||
async fn get_for_user(&self, _: &domain::value_objects::UserId, _: &domain::models::collections::PageParams) -> Result<domain::models::collections::Paginated<domain::models::WatchlistWithMovie>, DomainError> { panic!() }
|
||||
async fn contains(&self, _: &domain::value_objects::UserId, _: &domain::value_objects::MovieId) -> Result<bool, DomainError> { Ok(false) }
|
||||
async fn add(&self, _: &domain::models::WatchlistEntry) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn remove(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::MovieId,
|
||||
) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn remove_if_present(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::MovieId,
|
||||
) -> Result<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
async fn get_for_user(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::models::collections::PageParams,
|
||||
) -> Result<
|
||||
domain::models::collections::Paginated<domain::models::WatchlistWithMovie>,
|
||||
DomainError,
|
||||
> {
|
||||
panic!()
|
||||
}
|
||||
async fn contains(
|
||||
&self,
|
||||
_: &domain::value_objects::UserId,
|
||||
_: &domain::value_objects::MovieId,
|
||||
) -> Result<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::MovieProfileRepository for Panic {
|
||||
async fn upsert(&self, _: &domain::models::MovieProfile) -> Result<(), DomainError> { panic!() }
|
||||
async fn get_by_movie_id(&self, _: &domain::value_objects::MovieId) -> Result<Option<domain::models::MovieProfile>, DomainError> { Ok(None) }
|
||||
async fn list_stale(&self) -> Result<Vec<(domain::value_objects::MovieId, String)>, DomainError> { Ok(vec![]) }
|
||||
async fn upsert(&self, _: &domain::models::MovieProfile) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_by_movie_id(
|
||||
&self,
|
||||
_: &domain::value_objects::MovieId,
|
||||
) -> Result<Option<domain::models::MovieProfile>, DomainError> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn list_stale(
|
||||
&self,
|
||||
) -> Result<Vec<(domain::value_objects::MovieId, String)>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::DiaryExporter for Panic {
|
||||
@@ -272,10 +362,18 @@ impl domain::ports::DiaryExporter for Panic {
|
||||
}
|
||||
|
||||
impl domain::ports::DocumentParser for Panic {
|
||||
fn parse(&self, _: &[u8], _: domain::models::FileFormat) -> Result<domain::models::ParsedFile, domain::models::ImportError> {
|
||||
fn parse(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: domain::models::FileFormat,
|
||||
) -> Result<domain::models::ParsedFile, domain::models::ImportError> {
|
||||
panic!()
|
||||
}
|
||||
fn apply_mapping(&self, _: &domain::models::ParsedFile, _: &[domain::models::FieldMapping]) -> Vec<domain::models::AnnotatedRow> {
|
||||
fn apply_mapping(
|
||||
&self,
|
||||
_: &domain::models::ParsedFile,
|
||||
_: &[domain::models::FieldMapping],
|
||||
) -> Vec<domain::models::AnnotatedRow> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
@@ -312,10 +410,7 @@ impl crate::ports::HtmlRenderer for Panic {
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_users_page(
|
||||
&self,
|
||||
_: application::ports::UsersPageData,
|
||||
) -> Result<String, String> {
|
||||
fn render_users_page(&self, _: application::ports::UsersPageData) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_profile_page(
|
||||
@@ -342,13 +437,48 @@ impl crate::ports::HtmlRenderer for Panic {
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_import_upload_page(&self, _: application::ports::ImportUploadPageData) -> Result<String, String> { panic!() }
|
||||
fn render_import_mapping_page(&self, _: application::ports::ImportMappingPageData) -> Result<String, String> { panic!() }
|
||||
fn render_import_preview_page(&self, _: application::ports::ImportPreviewPageData) -> Result<String, String> { panic!() }
|
||||
fn render_profile_settings_page(&self, _: application::ports::ProfileSettingsPageData) -> Result<String, String> { panic!() }
|
||||
fn render_blocked_domains_page(&self, _: application::ports::BlockedDomainsPageData) -> Result<String, String> { panic!() }
|
||||
fn render_blocked_actors_page(&self, _: application::ports::BlockedActorsPageData) -> Result<String, String> { panic!() }
|
||||
fn render_watchlist_page(&self, _: application::ports::WatchlistPageData) -> Result<String, String> { panic!() }
|
||||
fn render_import_upload_page(
|
||||
&self,
|
||||
_: application::ports::ImportUploadPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_import_mapping_page(
|
||||
&self,
|
||||
_: application::ports::ImportMappingPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_import_preview_page(
|
||||
&self,
|
||||
_: application::ports::ImportPreviewPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_profile_settings_page(
|
||||
&self,
|
||||
_: application::ports::ProfileSettingsPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_blocked_domains_page(
|
||||
&self,
|
||||
_: application::ports::BlockedDomainsPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_blocked_actors_page(
|
||||
&self,
|
||||
_: application::ports::BlockedActorsPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
fn render_watchlist_page(
|
||||
&self,
|
||||
_: application::ports::WatchlistPageData,
|
||||
) -> Result<String, String> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
impl crate::ports::RssFeedRenderer for Panic {
|
||||
fn render_feed(&self, _: &[DiaryEntry], _: &str) -> Result<String, String> {
|
||||
@@ -369,32 +499,67 @@ impl AuthService for RejectingAuth {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PersonCommand for Panic {
|
||||
async fn upsert_batch(&self, _: &[Person]) -> Result<(), DomainError> { panic!() }
|
||||
async fn upsert_batch(&self, _: &[Person]) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl PersonQuery for Panic {
|
||||
async fn get_by_id(&self, _: &PersonId) -> Result<Option<Person>, DomainError> { panic!() }
|
||||
async fn get_by_external_id(&self, _: &domain::models::ExternalPersonId) -> Result<Option<Person>, DomainError> { panic!() }
|
||||
async fn get_credits(&self, _: &PersonId) -> Result<PersonCredits, DomainError> { panic!() }
|
||||
async fn list_orphaned_persons(&self) -> Result<Vec<PersonId>, DomainError> { panic!() }
|
||||
async fn get_by_id(&self, _: &PersonId) -> Result<Option<Person>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_by_external_id(
|
||||
&self,
|
||||
_: &domain::models::ExternalPersonId,
|
||||
) -> Result<Option<Person>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn get_credits(&self, _: &PersonId) -> Result<PersonCredits, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn list_orphaned_persons(&self) -> Result<Vec<PersonId>, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl SearchPort for Panic {
|
||||
async fn search(&self, _: &SearchQuery) -> Result<SearchResults, DomainError> { panic!() }
|
||||
async fn search(&self, _: &SearchQuery) -> Result<SearchResults, DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl SearchCommand for Panic {
|
||||
async fn index(&self, _: IndexableDocument) -> Result<(), DomainError> { panic!() }
|
||||
async fn remove(&self, _: EntityType, _: &str) -> Result<(), DomainError> { panic!() }
|
||||
async fn index(&self, _: IndexableDocument) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
async fn remove(&self, _: EntityType, _: &str) -> Result<(), DomainError> {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "federation")]
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::RemoteWatchlistRepository for Panic {
|
||||
async fn save(&self, _: domain::models::RemoteWatchlistEntry) -> Result<(), DomainError> { Ok(()) }
|
||||
async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> { Ok(()) }
|
||||
async fn get_by_actor_url(&self, _: &str) -> Result<Vec<domain::models::RemoteWatchlistEntry>, DomainError> { Ok(vec![]) }
|
||||
async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> { Ok(()) }
|
||||
async fn get_by_derived_uuid(&self, _: uuid::Uuid) -> Result<Vec<domain::models::RemoteWatchlistEntry>, DomainError> { Ok(vec![]) }
|
||||
async fn save(&self, _: domain::models::RemoteWatchlistEntry) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_by_ap_id(&self, _: &str, _: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_by_actor_url(
|
||||
&self,
|
||||
_: &str,
|
||||
) -> Result<Vec<domain::models::RemoteWatchlistEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn remove_all_by_actor(&self, _: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn get_by_derived_uuid(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
) -> Result<Vec<domain::models::RemoteWatchlistEntry>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
// --- Single state factory — only auth_service varies ---
|
||||
|
||||
Reference in New Issue
Block a user