refactor: split monolithic handlers + testing into domain-grouped modules
Some checks failed
CI / Check / Test (push) Has been cancelled
Some checks failed
CI / Check / Test (push) Has been cancelled
handlers/api.rs (1706 LOC) + html.rs (1735 LOC) → 12 domain files: auth, diary, movies, users, search, watchlist, goals, social, integrations, helpers + existing import/webhook/wrapup/images/rss. domain/testing.rs (1309 LOC) → testing/ module: in_memory, fakes, noops, panics, wrapup. Update README + architecture.mmd with goals feature.
This commit is contained in:
210
crates/presentation/src/handlers/integrations.rs
Normal file
210
crates/presentation/src/handlers/integrations.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::{Extension, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::integrations::{
|
||||
commands::{
|
||||
ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand,
|
||||
RevokeWebhookTokenCommand, WatchEventConfirmation,
|
||||
},
|
||||
confirm as confirm_watch_events, dismiss as dismiss_watch_events,
|
||||
generate_token as generate_webhook_token, get_queue as get_watch_queue,
|
||||
get_tokens as get_webhook_tokens,
|
||||
queries::{GetWatchQueueQuery, GetWebhookTokensQuery},
|
||||
revoke_token as revoke_webhook_token,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
csrf::CsrfToken, extractors::RequiredCookieUser, forms::ErrorQuery, render::render_page,
|
||||
state::AppState,
|
||||
};
|
||||
use template_askama::{IntegrationsTemplate, WatchQueueTemplate};
|
||||
|
||||
use super::helpers::build_page_context;
|
||||
|
||||
fn encode_error(msg: &str) -> String {
|
||||
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
|
||||
utf8_percent_encode(msg, NON_ALPHANUMERIC).to_string()
|
||||
}
|
||||
|
||||
// ── HTML ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn get_integrations_page(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<crate::forms::IntegrationsQuery>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> impl IntoResponse {
|
||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||
ctx.page_title = "Integrations — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/settings/integrations", state.app_ctx.config.base_url);
|
||||
|
||||
let query = GetWebhookTokensQuery {
|
||||
user_id: user_id.value(),
|
||||
};
|
||||
let tokens = get_webhook_tokens::execute(&state.app_ctx, query)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let token_views: Vec<template_askama::WebhookTokenView> = tokens
|
||||
.iter()
|
||||
.map(crate::mappers::integrations::webhook_token_view)
|
||||
.collect();
|
||||
|
||||
let webhook_base_url = state.app_ctx.config.base_url.clone();
|
||||
render_page(IntegrationsTemplate {
|
||||
ctx: &ctx,
|
||||
tokens: &token_views,
|
||||
webhook_base_url: &webhook_base_url,
|
||||
new_token: params.token.as_deref(),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn post_generate_token(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
Form(form): Form<crate::forms::GenerateTokenForm>,
|
||||
) -> impl IntoResponse {
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
let provider = match form.provider.parse::<domain::models::WatchEventSource>() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Redirect::to("/settings/integrations").into_response(),
|
||||
};
|
||||
|
||||
let cmd = GenerateWebhookTokenCommand {
|
||||
user_id: user_id.value(),
|
||||
provider,
|
||||
label: form.label.filter(|l| !l.trim().is_empty()),
|
||||
};
|
||||
|
||||
match generate_webhook_token::execute(&state.app_ctx, cmd).await {
|
||||
Ok(result) => {
|
||||
let encoded = percent_encoding::utf8_percent_encode(
|
||||
&result.token_plaintext,
|
||||
percent_encoding::NON_ALPHANUMERIC,
|
||||
);
|
||||
Redirect::to(&format!("/settings/integrations?token={encoded}")).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("generate token failed: {:?}", e);
|
||||
Redirect::to("/settings/integrations").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post_revoke_token(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(token_id): Path<Uuid>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
Form(form): Form<crate::forms::RevokeTokenForm>,
|
||||
) -> impl IntoResponse {
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
let cmd = RevokeWebhookTokenCommand {
|
||||
user_id: user_id.value(),
|
||||
token_id,
|
||||
};
|
||||
if let Err(e) = revoke_webhook_token::execute(&state.app_ctx, cmd).await {
|
||||
tracing::error!("revoke token failed: {:?}", e);
|
||||
}
|
||||
|
||||
Redirect::to("/settings/integrations").into_response()
|
||||
}
|
||||
|
||||
// ── Watch Queue ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn get_watch_queue_page(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ErrorQuery>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> impl IntoResponse {
|
||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||
ctx.page_title = "Watch Queue — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/watch-queue", state.app_ctx.config.base_url);
|
||||
|
||||
let query = GetWatchQueueQuery {
|
||||
user_id: user_id.value(),
|
||||
};
|
||||
let events = get_watch_queue::execute(&state.app_ctx, query)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let entries: Vec<template_askama::WatchQueueDisplayEntry> = events
|
||||
.iter()
|
||||
.map(crate::mappers::integrations::watch_queue_entry)
|
||||
.collect();
|
||||
|
||||
render_page(WatchQueueTemplate {
|
||||
ctx: &ctx,
|
||||
entries: &entries,
|
||||
error: params.error.as_deref(),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn post_confirm_single(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(event_id): Path<Uuid>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
Form(form): Form<crate::forms::ConfirmWatchForm>,
|
||||
) -> impl IntoResponse {
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
let cmd = ConfirmWatchEventsCommand {
|
||||
user_id: user_id.value(),
|
||||
confirmations: vec![WatchEventConfirmation {
|
||||
watch_event_id: event_id,
|
||||
rating: form.rating,
|
||||
comment: form.comment.filter(|c| !c.trim().is_empty()),
|
||||
}],
|
||||
};
|
||||
|
||||
match confirm_watch_events::execute(&state.app_ctx, cmd).await {
|
||||
Ok(_) => Redirect::to("/watch-queue").into_response(),
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!("/watch-queue?error={msg}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post_dismiss_single(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(event_id): Path<Uuid>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
Form(form): Form<crate::forms::DismissWatchForm>,
|
||||
) -> impl IntoResponse {
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
let cmd = DismissWatchEventsCommand {
|
||||
user_id: user_id.value(),
|
||||
event_ids: vec![event_id],
|
||||
};
|
||||
|
||||
match dismiss_watch_events::execute(&state.app_ctx, cmd).await {
|
||||
Ok(_) => Redirect::to("/watch-queue").into_response(),
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!("/watch-queue?error={msg}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user