refactor: LOW cleanups — dedup secure_flag, remove vestigial social_query,
drop PersistedImportSession, rename LoginQuery→LoginCommand, own DeleteAccountDeps, PersonId via uuid_id! macro, rename SortDirection→ ReviewSortBy, TUI fs::read→Command, generic PaginatedResponse<T>
This commit is contained in:
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{
|
models::{
|
||||||
DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, SortDirection,
|
DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::DiaryRepository,
|
ports::DiaryRepository,
|
||||||
@@ -45,15 +45,15 @@ impl PostgresDiaryRepository {
|
|||||||
|
|
||||||
async fn fetch_all_diary_rows(
|
async fn fetch_all_diary_rows(
|
||||||
&self,
|
&self,
|
||||||
sort: &SortDirection,
|
sort: &ReviewSortBy,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
let order = match sort {
|
let order = match sort {
|
||||||
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
SortDirection::Ascending => "r.watched_at ASC",
|
ReviewSortBy::Ascending => "r.watched_at ASC",
|
||||||
SortDirection::Descending => "r.watched_at DESC",
|
ReviewSortBy::Descending => "r.watched_at DESC",
|
||||||
};
|
};
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -79,15 +79,15 @@ impl PostgresDiaryRepository {
|
|||||||
async fn fetch_movie_diary_rows(
|
async fn fetch_movie_diary_rows(
|
||||||
&self,
|
&self,
|
||||||
movie_id: &str,
|
movie_id: &str,
|
||||||
sort: &SortDirection,
|
sort: &ReviewSortBy,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
let order = match sort {
|
let order = match sort {
|
||||||
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
SortDirection::Ascending => "r.watched_at ASC",
|
ReviewSortBy::Ascending => "r.watched_at ASC",
|
||||||
SortDirection::Descending => "r.watched_at DESC",
|
ReviewSortBy::Descending => "r.watched_at DESC",
|
||||||
};
|
};
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -144,7 +144,7 @@ impl PostgresDiaryRepository {
|
|||||||
async fn fetch_user_diary_rows(
|
async fn fetch_user_diary_rows(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
sort: &SortDirection,
|
sort: &ReviewSortBy,
|
||||||
search: Option<&str>,
|
search: Option<&str>,
|
||||||
include_remote: bool,
|
include_remote: bool,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
@@ -152,10 +152,10 @@ impl PostgresDiaryRepository {
|
|||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
let has_search = search.map(|s| !s.is_empty()).unwrap_or(false);
|
let has_search = search.map(|s| !s.is_empty()).unwrap_or(false);
|
||||||
let order_clause = match sort {
|
let order_clause = match sort {
|
||||||
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
SortDirection::Ascending => "r.watched_at ASC",
|
ReviewSortBy::Ascending => "r.watched_at ASC",
|
||||||
SortDirection::Descending => "r.watched_at DESC",
|
ReviewSortBy::Descending => "r.watched_at DESC",
|
||||||
};
|
};
|
||||||
let remote_clause = if include_remote {
|
let remote_clause = if include_remote {
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use domain::{
|
|||||||
models::{
|
models::{
|
||||||
AnnotatedRow, FieldMapping, ImportSession, ParsedFile,
|
AnnotatedRow, FieldMapping, ImportSession, ParsedFile,
|
||||||
import::{DomainField, ImportRow, RowResult, Transform},
|
import::{DomainField, ImportRow, RowResult, Transform},
|
||||||
import_session::PersistedImportSession,
|
|
||||||
},
|
},
|
||||||
ports::ImportSessionRepository,
|
ports::ImportSessionRepository,
|
||||||
value_objects::{ImportSessionId, UserId},
|
value_objects::{ImportSessionId, UserId},
|
||||||
@@ -267,7 +266,7 @@ impl PostgresImportSessionRepository {
|
|||||||
Ok(js.into_iter().map(annotated_from_json).collect())
|
Ok(js.into_iter().map(annotated_from_json).collect())
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
Ok(ImportSession::from_persistence(PersistedImportSession {
|
Ok(ImportSession {
|
||||||
id: ImportSessionId::from_uuid(
|
id: ImportSessionId::from_uuid(
|
||||||
id.parse::<uuid::Uuid>()
|
id.parse::<uuid::Uuid>()
|
||||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||||
@@ -282,7 +281,7 @@ impl PostgresImportSessionRepository {
|
|||||||
row_results,
|
row_results,
|
||||||
created_at,
|
created_at,
|
||||||
expires_at,
|
expires_at,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{
|
models::{
|
||||||
DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, SortDirection,
|
DiaryEntry, DiaryFilter, FeedEntry, MovieStats, ReviewHistory, ReviewSortBy,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::DiaryRepository,
|
ports::DiaryRepository,
|
||||||
@@ -45,15 +45,15 @@ impl SqliteDiaryRepository {
|
|||||||
|
|
||||||
async fn fetch_all_diary_rows(
|
async fn fetch_all_diary_rows(
|
||||||
&self,
|
&self,
|
||||||
sort: &SortDirection,
|
sort: &ReviewSortBy,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
let order_clause = match sort {
|
let order_clause = match sort {
|
||||||
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
SortDirection::Ascending => "r.watched_at ASC",
|
ReviewSortBy::Ascending => "r.watched_at ASC",
|
||||||
SortDirection::Descending => "r.watched_at DESC",
|
ReviewSortBy::Descending => "r.watched_at DESC",
|
||||||
};
|
};
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -75,15 +75,15 @@ impl SqliteDiaryRepository {
|
|||||||
async fn fetch_movie_diary_rows(
|
async fn fetch_movie_diary_rows(
|
||||||
&self,
|
&self,
|
||||||
movie_id: &str,
|
movie_id: &str,
|
||||||
sort: &SortDirection,
|
sort: &ReviewSortBy,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
let order_clause = match sort {
|
let order_clause = match sort {
|
||||||
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
SortDirection::Ascending => "r.watched_at ASC",
|
ReviewSortBy::Ascending => "r.watched_at ASC",
|
||||||
SortDirection::Descending => "r.watched_at DESC",
|
ReviewSortBy::Descending => "r.watched_at DESC",
|
||||||
};
|
};
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
@@ -136,7 +136,7 @@ impl SqliteDiaryRepository {
|
|||||||
async fn fetch_user_diary_rows(
|
async fn fetch_user_diary_rows(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
sort: &SortDirection,
|
sort: &ReviewSortBy,
|
||||||
search: Option<&str>,
|
search: Option<&str>,
|
||||||
include_remote: bool,
|
include_remote: bool,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
@@ -154,10 +154,10 @@ impl SqliteDiaryRepository {
|
|||||||
""
|
""
|
||||||
};
|
};
|
||||||
let order_clause = match sort {
|
let order_clause = match sort {
|
||||||
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
ReviewSortBy::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
ReviewSortBy::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
SortDirection::Ascending => "r.watched_at ASC",
|
ReviewSortBy::Ascending => "r.watched_at ASC",
|
||||||
SortDirection::Descending => "r.watched_at DESC",
|
ReviewSortBy::Descending => "r.watched_at DESC",
|
||||||
};
|
};
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use domain::{
|
|||||||
models::{
|
models::{
|
||||||
AnnotatedRow, FieldMapping, ImportSession, ParsedFile,
|
AnnotatedRow, FieldMapping, ImportSession, ParsedFile,
|
||||||
import::{DomainField, ImportRow, RowResult, Transform},
|
import::{DomainField, ImportRow, RowResult, Transform},
|
||||||
import_session::PersistedImportSession,
|
|
||||||
},
|
},
|
||||||
ports::ImportSessionRepository,
|
ports::ImportSessionRepository,
|
||||||
value_objects::{ImportSessionId, UserId},
|
value_objects::{ImportSessionId, UserId},
|
||||||
@@ -276,7 +275,7 @@ impl SqliteImportSessionRepository {
|
|||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
Ok(ImportSession::from_persistence(PersistedImportSession {
|
Ok(ImportSession {
|
||||||
id: ImportSessionId::from_uuid(
|
id: ImportSessionId::from_uuid(
|
||||||
id.parse::<uuid::Uuid>()
|
id.parse::<uuid::Uuid>()
|
||||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?,
|
||||||
@@ -291,7 +290,7 @@ impl SqliteImportSessionRepository {
|
|||||||
row_results,
|
row_results,
|
||||||
created_at: Self::parse_dt(created_at)?,
|
created_at: Self::parse_dt(created_at)?,
|
||||||
expires_at: Self::parse_dt(expires_at)?,
|
expires_at: Self::parse_dt(expires_at)?,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
pub struct PaginationQueryParams {
|
pub struct PaginationQueryParams {
|
||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
pub offset: Option<u32>,
|
pub offset: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct PaginatedResponse<T: std::fmt::Debug + Clone> {
|
||||||
|
pub items: Vec<T>,
|
||||||
|
pub total_count: u64,
|
||||||
|
pub limit: u32,
|
||||||
|
pub offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MoviesResponse = PaginatedResponse<crate::movies::MovieDto>;
|
||||||
|
pub type SocialFeedResponse = PaginatedResponse<crate::movies::SocialReviewDto>;
|
||||||
|
pub type DiaryResponse = PaginatedResponse<crate::diary::DiaryEntryDto>;
|
||||||
|
pub type ActivityFeedResponse = PaginatedResponse<crate::diary::FeedEntryDto>;
|
||||||
|
pub type WatchlistResponse = PaginatedResponse<crate::watchlist::WatchlistEntryDto>;
|
||||||
|
pub type PaginatedMovieHits = PaginatedResponse<crate::search::MovieSearchHitDto>;
|
||||||
|
pub type PaginatedPersonHits = PaginatedResponse<crate::search::PersonSearchHitDto>;
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ pub struct DiaryEntryDto {
|
|||||||
pub review: ReviewDto,
|
pub review: ReviewDto,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct DiaryResponse {
|
|
||||||
pub items: Vec<DiaryEntryDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
#[into_params(parameter_in = Query)]
|
#[into_params(parameter_in = Query)]
|
||||||
pub struct DiaryQueryParams {
|
pub struct DiaryQueryParams {
|
||||||
@@ -65,14 +57,6 @@ pub struct FeedEntryDto {
|
|||||||
pub actor_url: Option<String>,
|
pub actor_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct ActivityFeedResponse {
|
|
||||||
pub items: Vec<FeedEntryDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
#[into_params(parameter_in = Query)]
|
#[into_params(parameter_in = Query)]
|
||||||
pub struct ExportQueryParams {
|
pub struct ExportQueryParams {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::common::SocialFeedResponse;
|
||||||
|
|
||||||
// ── Movie list ────────────────────────────────────────────────────────────────
|
// ── Movie list ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
@@ -16,14 +18,6 @@ pub struct MoviesQueryParams {
|
|||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct MoviesResponse {
|
|
||||||
pub items: Vec<MovieDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Movie profile (enrichment) ────────────────────────────────────────────────
|
// ── Movie profile (enrichment) ────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
@@ -130,14 +124,6 @@ pub struct SocialReviewDto {
|
|||||||
pub watch_medium: Option<domain::value_objects::WatchMedium>,
|
pub watch_medium: Option<domain::value_objects::WatchMedium>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct SocialFeedResponse {
|
|
||||||
pub items: Vec<SocialReviewDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct MovieDetailResponse {
|
pub struct MovieDetailResponse {
|
||||||
pub movie: MovieDto,
|
pub movie: MovieDto,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use crate::common::{PaginatedMovieHits, PaginatedPersonHits};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, IntoParams)]
|
#[derive(Debug, Deserialize, IntoParams)]
|
||||||
pub struct SearchQueryParams {
|
pub struct SearchQueryParams {
|
||||||
/// Free-text query matched across title, cast, crew, genres and keywords.
|
/// Free-text query matched across title, cast, crew, genres and keywords.
|
||||||
@@ -28,23 +30,7 @@ pub struct SearchResponse {
|
|||||||
pub people: PaginatedPersonHits,
|
pub people: PaginatedPersonHits,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
pub struct PaginatedMovieHits {
|
|
||||||
pub items: Vec<MovieSearchHitDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
|
||||||
pub struct PaginatedPersonHits {
|
|
||||||
pub items: Vec<PersonSearchHitDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
|
||||||
pub struct MovieSearchHitDto {
|
pub struct MovieSearchHitDto {
|
||||||
pub movie_id: Uuid,
|
pub movie_id: Uuid,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -54,7 +40,7 @@ pub struct MovieSearchHitDto {
|
|||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
pub struct PersonSearchHitDto {
|
pub struct PersonSearchHitDto {
|
||||||
pub person_id: Uuid,
|
pub person_id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::diary::{DiaryEntryDto, DiaryResponse};
|
use crate::common::DiaryResponse;
|
||||||
|
use crate::diary::DiaryEntryDto;
|
||||||
use crate::goals::GoalDto;
|
use crate::goals::GoalDto;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ pub struct WatchlistEntryDto {
|
|||||||
pub added_at: String,
|
pub added_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct WatchlistResponse {
|
|
||||||
pub items: Vec<WatchlistEntryDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct AddToWatchlistRequest {
|
pub struct AddToWatchlistRequest {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use domain::{errors::DomainError, models::RefreshSession, value_objects::Email};
|
use domain::{errors::DomainError, models::RefreshSession, value_objects::Email};
|
||||||
|
|
||||||
use crate::auth::{deps::LoginDeps, queries::LoginQuery};
|
use crate::auth::{deps::LoginDeps, queries::LoginCommand};
|
||||||
|
|
||||||
pub struct LoginResult {
|
pub struct LoginResult {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
@@ -14,7 +14,7 @@ pub struct LoginResult {
|
|||||||
pub role: domain::models::UserRole,
|
pub role: domain::models::UserRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(deps: &LoginDeps, query: LoginQuery) -> Result<LoginResult, DomainError> {
|
pub async fn execute(deps: &LoginDeps, query: LoginCommand) -> Result<LoginResult, DomainError> {
|
||||||
let email = Email::new(query.email)?;
|
let email = Email::new(query.email)?;
|
||||||
let user = deps
|
let user = deps
|
||||||
.user
|
.user
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pub struct LoginQuery {
|
pub struct LoginCommand {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::auth::{
|
|||||||
commands::{RegisterAndLoginCommand, RegisterCommand},
|
commands::{RegisterAndLoginCommand, RegisterCommand},
|
||||||
deps::{LoginDeps, RegisterAndLoginDeps, RegisterDeps},
|
deps::{LoginDeps, RegisterAndLoginDeps, RegisterDeps},
|
||||||
login::{self, LoginResult},
|
login::{self, LoginResult},
|
||||||
queries::LoginQuery,
|
queries::LoginCommand,
|
||||||
register,
|
register,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ pub async fn execute(
|
|||||||
};
|
};
|
||||||
login::execute(
|
login::execute(
|
||||||
&log_deps,
|
&log_deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: cmd.email,
|
email: cmd.email,
|
||||||
password: cmd.password,
|
password: cmd.password,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
commands::RegisterCommand,
|
commands::RegisterCommand,
|
||||||
deps::{LoginDeps, RegisterDeps},
|
deps::{LoginDeps, RegisterDeps},
|
||||||
login,
|
login,
|
||||||
queries::LoginQuery,
|
queries::LoginCommand,
|
||||||
register,
|
register,
|
||||||
},
|
},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
@@ -48,7 +48,7 @@ async fn test_login_valid_credentials_returns_token() {
|
|||||||
};
|
};
|
||||||
let result = login::execute(
|
let result = login::execute(
|
||||||
&deps,
|
&deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: "carol@example.com".into(),
|
email: "carol@example.com".into(),
|
||||||
password: "secret123".into(),
|
password: "secret123".into(),
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ async fn test_login_wrong_password_fails() {
|
|||||||
};
|
};
|
||||||
let result = login::execute(
|
let result = login::execute(
|
||||||
&deps,
|
&deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: "dave@example.com".into(),
|
email: "dave@example.com".into(),
|
||||||
password: "wrong_password".into(),
|
password: "wrong_password".into(),
|
||||||
},
|
},
|
||||||
@@ -98,7 +98,7 @@ async fn test_login_unknown_email_fails() {
|
|||||||
};
|
};
|
||||||
let result = login::execute(
|
let result = login::execute(
|
||||||
&deps,
|
&deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: "nobody@example.com".into(),
|
email: "nobody@example.com".into(),
|
||||||
password: "anything".into(),
|
password: "anything".into(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
commands::RegisterCommand,
|
commands::RegisterCommand,
|
||||||
deps::{LoginDeps, RefreshDeps, RegisterDeps},
|
deps::{LoginDeps, RefreshDeps, RegisterDeps},
|
||||||
login, logout,
|
login, logout,
|
||||||
queries::LoginQuery,
|
queries::LoginCommand,
|
||||||
refresh, register,
|
refresh, register,
|
||||||
},
|
},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
@@ -45,7 +45,7 @@ async fn logout_revokes_refresh_token() {
|
|||||||
};
|
};
|
||||||
let login_result = login::execute(
|
let login_result = login::execute(
|
||||||
&login_deps,
|
&login_deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: "bob@example.com".into(),
|
email: "bob@example.com".into(),
|
||||||
password: "password123".into(),
|
password: "password123".into(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
commands::RegisterCommand,
|
commands::RegisterCommand,
|
||||||
deps::{LoginDeps, RefreshDeps, RegisterDeps},
|
deps::{LoginDeps, RefreshDeps, RegisterDeps},
|
||||||
login,
|
login,
|
||||||
queries::LoginQuery,
|
queries::LoginCommand,
|
||||||
refresh, register,
|
refresh, register,
|
||||||
},
|
},
|
||||||
test_helpers::TestContextBuilder,
|
test_helpers::TestContextBuilder,
|
||||||
@@ -41,7 +41,7 @@ async fn login_user(b: &TestContextBuilder) -> login::LoginResult {
|
|||||||
};
|
};
|
||||||
login::execute(
|
login::execute(
|
||||||
&login_deps,
|
&login_deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: "alice@example.com".into(),
|
email: "alice@example.com".into(),
|
||||||
password: "password123".into(),
|
password: "password123".into(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::{
|
models::{
|
||||||
DiaryEntry, DiaryFilter, SortDirection,
|
DiaryEntry, DiaryFilter, ReviewSortBy,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
ports::DiaryRepository,
|
ports::DiaryRepository,
|
||||||
@@ -21,7 +21,7 @@ pub async fn execute(
|
|||||||
let user_id = query.user_id.map(UserId::from_uuid);
|
let user_id = query.user_id.map(UserId::from_uuid);
|
||||||
|
|
||||||
let filter = DiaryFilter {
|
let filter = DiaryFilter {
|
||||||
sort_by: query.sort_by.unwrap_or(SortDirection::Descending),
|
sort_by: query.sort_by.unwrap_or(ReviewSortBy::Descending),
|
||||||
page,
|
page,
|
||||||
movie_id,
|
movie_id,
|
||||||
user_id: user_id.clone(),
|
user_id: user_id.clone(),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use domain::models::SortDirection;
|
use domain::models::ReviewSortBy;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct GetDiaryQuery {
|
pub struct GetDiaryQuery {
|
||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
pub offset: Option<u32>,
|
pub offset: Option<u32>,
|
||||||
pub sort_by: Option<SortDirection>,
|
pub sort_by: Option<ReviewSortBy>,
|
||||||
pub movie_id: Option<Uuid>,
|
pub movie_id: Option<Uuid>,
|
||||||
pub user_id: Option<Uuid>,
|
pub user_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
||||||
|
|
||||||
use crate::users::deps::UpdateProfileDeps;
|
use crate::users::deps::DeleteAccountDeps;
|
||||||
|
|
||||||
pub async fn execute(deps: &UpdateProfileDeps, user_id: uuid::Uuid) -> Result<(), DomainError> {
|
pub async fn execute(deps: &DeleteAccountDeps, user_id: uuid::Uuid) -> Result<(), DomainError> {
|
||||||
let uid = UserId::from_uuid(user_id);
|
let uid = UserId::from_uuid(user_id);
|
||||||
|
|
||||||
deps.user
|
deps.user
|
||||||
|
|||||||
@@ -16,3 +16,8 @@ pub struct UpdateProfileDeps {
|
|||||||
pub object_storage: Arc<dyn ObjectStorage>,
|
pub object_storage: Arc<dyn ObjectStorage>,
|
||||||
pub event_publisher: Arc<dyn EventPublisher>,
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DeleteAccountDeps {
|
||||||
|
pub user: Arc<dyn UserRepository>,
|
||||||
|
pub event_publisher: Arc<dyn EventPublisher>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::FeedSortBy,
|
models::FeedSortBy,
|
||||||
models::{
|
models::{
|
||||||
DiaryEntry, DiaryFilter, SortDirection, UserStats, UserTrends,
|
DiaryEntry, DiaryFilter, ReviewSortBy, UserStats, UserTrends,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
@@ -108,18 +108,18 @@ async fn load_social_counts(
|
|||||||
(following, followers, pending)
|
(following, followers, pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn feed_sort_to_direction(sort_by: FeedSortBy) -> SortDirection {
|
fn feed_sort_to_direction(sort_by: FeedSortBy) -> ReviewSortBy {
|
||||||
match sort_by {
|
match sort_by {
|
||||||
FeedSortBy::Date => SortDirection::Descending,
|
FeedSortBy::Date => ReviewSortBy::Descending,
|
||||||
FeedSortBy::DateAsc => SortDirection::Ascending,
|
FeedSortBy::DateAsc => ReviewSortBy::Ascending,
|
||||||
FeedSortBy::Rating => SortDirection::ByRatingDesc,
|
FeedSortBy::Rating => ReviewSortBy::ByRatingDesc,
|
||||||
FeedSortBy::RatingAsc => SortDirection::ByRatingAsc,
|
FeedSortBy::RatingAsc => ReviewSortBy::ByRatingAsc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paged_user_filter(
|
fn paged_user_filter(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
sort_by: SortDirection,
|
sort_by: ReviewSortBy,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
search: Option<String>,
|
search: Option<String>,
|
||||||
@@ -149,19 +149,19 @@ mod helper_tests {
|
|||||||
use domain::models::FeedSortBy;
|
use domain::models::FeedSortBy;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
feed_sort_to_direction(FeedSortBy::Date),
|
feed_sort_to_direction(FeedSortBy::Date),
|
||||||
SortDirection::Descending
|
ReviewSortBy::Descending
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
feed_sort_to_direction(FeedSortBy::DateAsc),
|
feed_sort_to_direction(FeedSortBy::DateAsc),
|
||||||
SortDirection::Ascending
|
ReviewSortBy::Ascending
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
feed_sort_to_direction(FeedSortBy::Rating),
|
feed_sort_to_direction(FeedSortBy::Rating),
|
||||||
SortDirection::ByRatingDesc
|
ReviewSortBy::ByRatingDesc
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
feed_sort_to_direction(FeedSortBy::RatingAsc),
|
feed_sort_to_direction(FeedSortBy::RatingAsc),
|
||||||
SortDirection::ByRatingAsc
|
ReviewSortBy::ByRatingAsc
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ mod helper_tests {
|
|||||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||||
let filter = paged_user_filter(
|
let filter = paged_user_filter(
|
||||||
uid.clone(),
|
uid.clone(),
|
||||||
SortDirection::Descending,
|
ReviewSortBy::Descending,
|
||||||
Some(20),
|
Some(20),
|
||||||
Some(5),
|
Some(5),
|
||||||
Some("blade".into()),
|
Some("blade".into()),
|
||||||
|
|||||||
@@ -15,16 +15,6 @@ pub struct ImportSession {
|
|||||||
pub expires_at: NaiveDateTime,
|
pub expires_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PersistedImportSession {
|
|
||||||
pub id: ImportSessionId,
|
|
||||||
pub user_id: UserId,
|
|
||||||
pub parsed_file: Option<ParsedFile>,
|
|
||||||
pub field_mappings: Option<Vec<FieldMapping>>,
|
|
||||||
pub row_results: Option<Vec<AnnotatedRow>>,
|
|
||||||
pub created_at: NaiveDateTime,
|
|
||||||
pub expires_at: NaiveDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImportSession {
|
impl ImportSession {
|
||||||
pub fn new(user_id: UserId) -> Self {
|
pub fn new(user_id: UserId) -> Self {
|
||||||
let created_at = chrono::Utc::now().naive_utc();
|
let created_at = chrono::Utc::now().naive_utc();
|
||||||
@@ -39,16 +29,4 @@ impl ImportSession {
|
|||||||
expires_at,
|
expires_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_persistence(p: PersistedImportSession) -> Self {
|
|
||||||
Self {
|
|
||||||
id: p.id,
|
|
||||||
user_id: p.user_id,
|
|
||||||
parsed_file: p.parsed_file,
|
|
||||||
field_mappings: p.field_mappings,
|
|
||||||
row_results: p.row_results,
|
|
||||||
created_at: p.created_at,
|
|
||||||
expires_at: p.expires_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ pub use import::{
|
|||||||
pub use import_profile::ImportProfile;
|
pub use import_profile::ImportProfile;
|
||||||
pub use import_session::ImportSession;
|
pub use import_session::ImportSession;
|
||||||
pub use person::{
|
pub use person::{
|
||||||
CastCredit, CrewCredit, ExternalPersonId, Person, PersonCredits, PersonEnrichmentData, PersonId,
|
CastCredit, CrewCredit, ExternalPersonId, Person, PersonCredits, PersonEnrichmentData,
|
||||||
};
|
};
|
||||||
|
pub use crate::value_objects::PersonId;
|
||||||
pub use refresh_session::{GeneratedToken, RefreshSession};
|
pub use refresh_session::{GeneratedToken, RefreshSession};
|
||||||
pub use search::{
|
pub use search::{
|
||||||
EntityType, IndexableDocument, MovieSearchHit, PersonSearchHit, SearchFilters, SearchQuery,
|
EntityType, IndexableDocument, MovieSearchHit, PersonSearchHit, SearchFilters, SearchQuery,
|
||||||
@@ -85,7 +86,7 @@ impl std::str::FromStr for GoalType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum SortDirection {
|
pub enum ReviewSortBy {
|
||||||
#[default]
|
#[default]
|
||||||
Descending,
|
Descending,
|
||||||
Ascending,
|
Ascending,
|
||||||
|
|||||||
@@ -1,25 +1,4 @@
|
|||||||
use uuid::Uuid;
|
use crate::value_objects::{MovieId, PersonId};
|
||||||
|
|
||||||
use crate::value_objects::MovieId;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct PersonId(Uuid);
|
|
||||||
|
|
||||||
impl PersonId {
|
|
||||||
pub fn from_uuid(uuid: Uuid) -> Self {
|
|
||||||
Self(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deterministic UUIDv5 from an external person ID string.
|
|
||||||
/// "tmdb:12345" always maps to the same PersonId.
|
|
||||||
pub fn from_external(external_id: &ExternalPersonId) -> Self {
|
|
||||||
Self(Uuid::new_v5(&Uuid::NAMESPACE_URL, external_id.0.as_bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn value(&self) -> Uuid {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ExternalPersonId(String);
|
pub struct ExternalPersonId(String);
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ impl DiaryEntry {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct DiaryFilter {
|
pub struct DiaryFilter {
|
||||||
pub sort_by: super::SortDirection,
|
pub sort_by: super::ReviewSortBy,
|
||||||
pub page: crate::models::collections::PageParams,
|
pub page: crate::models::collections::PageParams,
|
||||||
pub movie_id: Option<MovieId>,
|
pub movie_id: Option<MovieId>,
|
||||||
pub user_id: Option<UserId>,
|
pub user_id: Option<UserId>,
|
||||||
|
|||||||
@@ -42,3 +42,12 @@ uuid_id!(WatchEventId);
|
|||||||
uuid_id!(WebhookTokenId);
|
uuid_id!(WebhookTokenId);
|
||||||
uuid_id!(WrapUpId);
|
uuid_id!(WrapUpId);
|
||||||
uuid_id!(GoalId);
|
uuid_id!(GoalId);
|
||||||
|
uuid_id!(PersonId);
|
||||||
|
|
||||||
|
impl PersonId {
|
||||||
|
/// Deterministic UUIDv5 from an external person ID string.
|
||||||
|
/// "tmdb:12345" always maps to the same PersonId.
|
||||||
|
pub fn from_external(external_id: &crate::models::person::ExternalPersonId) -> Self {
|
||||||
|
Self(Uuid::new_v5(&Uuid::NAMESPACE_URL, external_id.value().as_bytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn secure_flag() -> &'static str {
|
pub(crate) fn secure_flag() -> &'static str {
|
||||||
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
|
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
|
||||||
"; Secure"
|
"; Secure"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use application::diary::{
|
|||||||
commands::{LogReviewCommand, MovieInput},
|
commands::{LogReviewCommand, MovieInput},
|
||||||
queries::GetDiaryQuery,
|
queries::GetDiaryQuery,
|
||||||
};
|
};
|
||||||
use domain::{errors::DomainError, models::SortDirection};
|
use domain::{errors::DomainError, models::ReviewSortBy};
|
||||||
|
|
||||||
use api_types::{DiaryQueryParams, LogReviewRequest};
|
use api_types::{DiaryQueryParams, LogReviewRequest};
|
||||||
|
|
||||||
@@ -258,10 +258,10 @@ pub fn to_diary_query(p: DiaryQueryParams) -> GetDiaryQuery {
|
|||||||
limit: p.limit,
|
limit: p.limit,
|
||||||
offset: p.offset,
|
offset: p.offset,
|
||||||
sort_by: p.sort_by.as_deref().map(|s| match s {
|
sort_by: p.sort_by.as_deref().map(|s| match s {
|
||||||
"date_asc" | "asc" => SortDirection::Ascending,
|
"date_asc" | "asc" => ReviewSortBy::Ascending,
|
||||||
"rating_desc" => SortDirection::ByRatingDesc,
|
"rating_desc" => ReviewSortBy::ByRatingDesc,
|
||||||
"rating_asc" => SortDirection::ByRatingAsc,
|
"rating_asc" => ReviewSortBy::ByRatingAsc,
|
||||||
_ => SortDirection::Descending,
|
_ => ReviewSortBy::Descending,
|
||||||
}),
|
}),
|
||||||
movie_id: p.movie_id,
|
movie_id: p.movie_id,
|
||||||
user_id: p.user_id,
|
user_id: p.user_id,
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ use application::auth::{
|
|||||||
commands::RegisterCommand,
|
commands::RegisterCommand,
|
||||||
deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps},
|
deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps},
|
||||||
login as login_uc,
|
login as login_uc,
|
||||||
queries::LoginQuery,
|
queries::LoginCommand,
|
||||||
register as register_uc,
|
register as register_uc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
csrf::CsrfToken,
|
csrf::{CsrfToken, secure_flag},
|
||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
forms::{ErrorQuery, LoginForm, RegisterForm},
|
forms::{ErrorQuery, LoginForm, RegisterForm},
|
||||||
render::render_page,
|
render::render_page,
|
||||||
@@ -29,14 +29,6 @@ use template_askama::{LoginTemplate, RegisterTemplate};
|
|||||||
|
|
||||||
// ── HTML helpers ─────────────────────────────────────────────────────────────
|
// ── HTML helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn secure_flag() -> &'static str {
|
|
||||||
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
|
|
||||||
"; Secure"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) {
|
fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) {
|
||||||
let val = format!(
|
let val = format!(
|
||||||
"token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}",
|
"token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}",
|
||||||
@@ -73,7 +65,7 @@ pub async fn login(
|
|||||||
};
|
};
|
||||||
let result = login_uc::execute(
|
let result = login_uc::execute(
|
||||||
&deps,
|
&deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: req.email,
|
email: req.email,
|
||||||
password: req.password,
|
password: req.password,
|
||||||
},
|
},
|
||||||
@@ -204,7 +196,7 @@ pub async fn post_login(
|
|||||||
};
|
};
|
||||||
match login_uc::execute(
|
match login_uc::execute(
|
||||||
&deps,
|
&deps,
|
||||||
LoginQuery {
|
LoginCommand {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use axum::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use application::{diary::get_diary, diary::queries::GetDiaryQuery};
|
use application::{diary::get_diary, diary::queries::GetDiaryQuery};
|
||||||
use domain::{errors::DomainError, models::SortDirection, value_objects::UserId};
|
use domain::{errors::DomainError, models::ReviewSortBy, value_objects::UserId};
|
||||||
|
|
||||||
use crate::{errors::ApiError, state::AppState};
|
use crate::{errors::ApiError, state::AppState};
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ pub async fn get_feed(State(state): State<AppState>) -> Result<impl IntoResponse
|
|||||||
let query = GetDiaryQuery {
|
let query = GetDiaryQuery {
|
||||||
limit: Some(super::RSS_FEED_LIMIT),
|
limit: Some(super::RSS_FEED_LIMIT),
|
||||||
offset: Some(0),
|
offset: Some(0),
|
||||||
sort_by: Some(SortDirection::Descending),
|
sort_by: Some(ReviewSortBy::Descending),
|
||||||
movie_id: None,
|
movie_id: None,
|
||||||
user_id: None,
|
user_id: None,
|
||||||
};
|
};
|
||||||
@@ -45,7 +45,7 @@ pub async fn get_user_feed(
|
|||||||
let query = GetDiaryQuery {
|
let query = GetDiaryQuery {
|
||||||
limit: Some(super::RSS_FEED_LIMIT),
|
limit: Some(super::RSS_FEED_LIMIT),
|
||||||
offset: Some(0),
|
offset: Some(0),
|
||||||
sort_by: Some(SortDirection::Descending),
|
sort_by: Some(ReviewSortBy::Descending),
|
||||||
movie_id: None,
|
movie_id: None,
|
||||||
user_id: Some(user_id),
|
user_id: Some(user_id),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -209,8 +209,6 @@ async fn wire_dependencies() -> anyhow::Result<(AppState, axum::Router)> {
|
|||||||
)),
|
)),
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
ap_service,
|
ap_service,
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
social_query,
|
|
||||||
};
|
};
|
||||||
Ok((state, ap_router))
|
Ok((state, ap_router))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,4 @@ pub struct AppState {
|
|||||||
pub rss_renderer: Arc<dyn RssFeedRenderer>,
|
pub rss_renderer: Arc<dyn RssFeedRenderer>,
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
pub ap_service: Arc<dyn activitypub::ActivityPubPort>,
|
pub ap_service: Arc<dyn activitypub::ActivityPubPort>,
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
pub social_query: Arc<dyn domain::ports::SocialQueryPort>,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -843,8 +843,6 @@ pub fn make_test_state(auth_service: Arc<dyn AuthService>) -> crate::state::AppS
|
|||||||
rss_renderer: Arc::new(Panic),
|
rss_renderer: Arc::new(Panic),
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
ap_service: Arc::new(activitypub::NoopActivityPubService),
|
ap_service: Arc::new(activitypub::NoopActivityPubService),
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
social_query: Arc::new(Panic),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ fn sort_by_asc_string_becomes_ascending() {
|
|||||||
let query = to_diary_query(params);
|
let query = to_diary_query(params);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
query.sort_by,
|
query.sort_by,
|
||||||
Some(domain::models::SortDirection::Ascending)
|
Some(domain::models::ReviewSortBy::Ascending)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ fn sort_by_other_string_becomes_descending() {
|
|||||||
let query = to_diary_query(params);
|
let query = to_diary_query(params);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
query.sort_by,
|
query.sort_by,
|
||||||
Some(domain::models::SortDirection::Descending)
|
Some(domain::models::ReviewSortBy::Descending)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -500,8 +500,6 @@ async fn test_app() -> Router {
|
|||||||
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
|
rss_renderer: Arc::new(RssAdapter::new("http://localhost:3000".into())),
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
ap_service: Arc::new(activitypub::NoopActivityPubService),
|
ap_service: Arc::new(activitypub::NoopActivityPubService),
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
social_query: Arc::new(PanicSocialQuery),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
routes::build_router(state, axum::Router::new())
|
routes::build_router(state, axum::Router::new())
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ pub enum Action {
|
|||||||
ReviewCreateFailed(String),
|
ReviewCreateFailed(String),
|
||||||
ReviewDeleted(Uuid),
|
ReviewDeleted(Uuid),
|
||||||
ReviewDeleteFailed(String),
|
ReviewDeleteFailed(String),
|
||||||
|
FileRead(String),
|
||||||
|
FileReadFailed(String),
|
||||||
BulkItemDone {
|
BulkItemDone {
|
||||||
index: usize,
|
index: usize,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
@@ -265,6 +267,7 @@ pub enum Command {
|
|||||||
SaveConfig(String),
|
SaveConfig(String),
|
||||||
SaveToken(String),
|
SaveToken(String),
|
||||||
ClearToken,
|
ClearToken,
|
||||||
|
ReadFile { path: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches the export CSV column order:
|
// Matches the export CSV column order:
|
||||||
@@ -863,22 +866,34 @@ pub fn update(app: &mut App, action: Action) -> Vec<Command> {
|
|||||||
&& m.bulk_import.stage == BulkImportStage::EnterPath
|
&& m.bulk_import.stage == BulkImportStage::EnterPath
|
||||||
{
|
{
|
||||||
let path = m.bulk_import.file_path.trim().to_string();
|
let path = m.bulk_import.file_path.trim().to_string();
|
||||||
match std::fs::read_to_string(&path) {
|
if path.is_empty() {
|
||||||
Ok(content) => {
|
app.status = Some(StatusMsg {
|
||||||
m.bulk_import.parsed = parse_csv(&content);
|
text: "File path required".into(),
|
||||||
m.bulk_import.stage = BulkImportStage::Preview;
|
is_error: true,
|
||||||
}
|
});
|
||||||
Err(e) => {
|
return vec![];
|
||||||
app.status = Some(StatusMsg {
|
|
||||||
text: format!("Cannot read file: {e}"),
|
|
||||||
is_error: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return vec![Command::ReadFile { path }];
|
||||||
}
|
}
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Action::FileRead(content) => {
|
||||||
|
if let Screen::Main(m) = &mut app.screen {
|
||||||
|
m.bulk_import.parsed = parse_csv(&content);
|
||||||
|
m.bulk_import.stage = BulkImportStage::Preview;
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
Action::FileReadFailed(msg) => {
|
||||||
|
app.status = Some(StatusMsg {
|
||||||
|
text: format!("Cannot read file: {msg}"),
|
||||||
|
is_error: true,
|
||||||
|
});
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
Action::BulkImportAll => {
|
Action::BulkImportAll => {
|
||||||
if let Screen::Main(m) = &mut app.screen
|
if let Screen::Main(m) = &mut app.screen
|
||||||
&& m.tab == Tab::BulkImport
|
&& m.tab == Tab::BulkImport
|
||||||
|
|||||||
@@ -220,6 +220,21 @@ fn handle_command(cmd: Command, app: &App, client: &Arc<ApiClient>, tx: &mpsc::S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Command::ReadFile { path } => {
|
||||||
|
let tx = tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let action =
|
||||||
|
match tokio::task::spawn_blocking(move || std::fs::read_to_string(&path))
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| Err(std::io::Error::other(e)))
|
||||||
|
{
|
||||||
|
Ok(content) => Action::FileRead(content),
|
||||||
|
Err(e) => Action::FileReadFailed(e.to_string()),
|
||||||
|
};
|
||||||
|
let _ = tx.send(action).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Command::ImportNext(index) => {
|
Command::ImportNext(index) => {
|
||||||
let Some(token) = app.token.clone() else {
|
let Some(token) = app.token.clone() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user