//! JSON tam hane yedeği (`services::backup`). //! //! | Yöntem | Yol | Kim | İş | //! |---|---|---|---| //! | GET | `/api/backup/download` | üye | yedek dosyası (indirme) | //! | GET | `/api/backup/status` | üye | son yedek zamanı + geri dönüş noktaları (yalnız sahip görür) | //! | POST | `/api/backup/preview` | sahip | gövde: yedek dosyası → ne değişecek (hiçbir şey yazılmaz) | //! | POST | `/api/backup/restore` | sahip | gövde: yedek dosyası → hane verisi değiştirilir | //! | POST | `/api/backup/restore-points/{id}/restore` | sahip | geri dönüş noktasına dön ("geri al") | use axum::{ Json, body::Bytes, extract::{Path, State}, http::header, response::IntoResponse, }; use mongodb::bson::DateTime; use serde::Serialize; use ts_rs::TS; use crate::{ auth::AuthUser, error::{AppError, AppResult}, models::user::Role, services::{ backup::{self, Reason}, calendar::{format_date, today_tr}, }, state::AppState, }; fn iso(d: DateTime) -> String { d.try_to_rfc3339_string().unwrap_or_default() } fn parse_id(id: &str) -> AppResult { mongodb::bson::Uuid::parse_str(id).map_err(|_| AppError::NotFound) } pub async fn download(State(s): State, auth: AuthUser) -> AppResult { let snap = backup::collect(&s.db, auth.household_id).await?; let body = backup::to_json(&snap)?; backup::mark_exported(&s.db, auth.household_id).await?; let file = format!( "kakeibo-yedek-{}-{}.json", backup::slug(&snap.household_name), format_date(today_tr()) ); Ok(( [ ( header::CONTENT_TYPE, "application/json; charset=utf-8".to_string(), ), ( header::CONTENT_DISPOSITION, format!("attachment; filename=\"{file}\""), ), (header::CACHE_CONTROL, "no-store".to_string()), ], body, )) } #[derive(Serialize, TS)] #[ts(export)] pub struct RestorePoint { id: String, /// RFC 3339 created_at: String, /// `before_restore`: dosyadan geri yüklemeden önce · `before_undo`: geri almadan önce #[ts(type = "\"before_restore\" | \"before_undo\"")] reason: String, #[ts(type = "number")] periods: i64, #[ts(type = "number")] entries: i64, /// Sıkıştırılmış boyut (bayt) #[ts(type = "number")] size: i64, } #[derive(Serialize, TS)] #[ts(export)] pub struct BackupStatus { /// Son dışa aktarma (RFC 3339); hiç alınmadıysa `null` last_backup_at: Option, /// En yeniden eskiye; sahip olmayana boş restore_points: Vec, keep_points: u32, keep_days: u32, } pub async fn status(State(s): State, auth: AuthUser) -> AppResult> { let points = if auth.role == Role::Owner { backup::points(&s.db, auth.household_id).await? } else { Vec::new() }; Ok(Json(BackupStatus { last_backup_at: backup::last_exported(&s.db, auth.household_id) .await? .map(iso), restore_points: points .into_iter() .map(|p| RestorePoint { id: p.id.to_string(), created_at: iso(p.created_at), reason: p.reason, periods: p.periods, entries: p.entries, size: p.size, }) .collect(), keep_points: backup::KEEP_POINTS as u32, keep_days: backup::KEEP_DAYS as u32, })) } #[derive(Serialize, TS)] #[ts(export)] pub struct BackupCount { /// Koleksiyon adı (`entries`, `periods`, …) kind: String, /// Türkçe ad label: String, #[ts(type = "number")] backup: i64, #[ts(type = "number")] current: i64, } #[derive(Serialize, TS)] #[ts(export)] pub struct BackupPreview { source_name: String, same_household: bool, /// RFC 3339 exported_at: String, app_version: String, counts: Vec, /// Yedekteki dönemlerin ilk ve son günü first_date: Option, last_date: Option, /// Aynı hane: yedekten sonra eklenen/değişen (kaybolacak) kayıt #[ts(type = "number")] changed_since: i64, warnings: Vec, } pub async fn preview( State(s): State, auth: AuthUser, body: Bytes, ) -> AppResult> { auth.require_owner()?; let snap = backup::parse(&body)?; let p = backup::preview(&s.db, auth.household_id, &snap).await?; Ok(Json(BackupPreview { source_name: p.source_name, same_household: p.same_household, exported_at: iso(p.exported_at), app_version: p.app_version, counts: p .counts .into_iter() .map(|c| BackupCount { kind: c.kind.into(), label: c.label.into(), backup: c.backup, current: c.current, }) .collect(), first_date: p.first_date, last_date: p.last_date, changed_since: p.changed_since, warnings: p.warnings, })) } #[derive(Serialize, TS)] #[ts(export)] pub struct RestoreResult { /// İşlemden önceki durumun geri dönüş noktası ("Geri al" bununla) restore_point_id: String, } pub async fn restore( State(s): State, auth: AuthUser, body: Bytes, ) -> AppResult> { auth.require_owner()?; let snap = backup::parse(&body)?; let id = backup::restore( &s.db, auth.household_id, auth.user_id, &snap, Reason::BeforeRestore, ) .await?; Ok(Json(RestoreResult { restore_point_id: id.to_string(), })) } pub async fn undo( State(s): State, auth: AuthUser, Path(id): Path, ) -> AppResult> { auth.require_owner()?; let id = backup::undo(&s.db, auth.household_id, auth.user_id, parse_id(&id)?).await?; Ok(Json(RestoreResult { restore_point_id: id.to_string(), })) }