Files
Holzleitner---Backend--aktu…/crates/api/src/routes/attachments.rs
Dennis Nemec 6a9b5872e1 Backend-Arbeitsstand: ERP-Sync, Lieferlebenszyklus, Reports + config.toml
Bringt das Backend vom initialen Skeleton auf den aktuellen Arbeitsstand
(Clean Architecture: domain → application → infrastructure → api).

Wesentliche Bereiche:
- ERP-Anbindung (MSSQL-Pull der Touren, Import-Scheduler, Rückschreiben)
- Lieferlebenszyklus: Scan/Hold/Cancel/Complete, Gutschriften, Notizen,
  Bild-Anhänge, Unterschriften, PDF-Lieferreport → DOCUframe
- Stammdaten: Kunden, Artikel, Lager, Zahlungsarten, Services
- Keycloak-JWT-Gate + Fahrer-Provisionierung via Admin-API
- Admin-API-Key-Gate (X-Admin-Api-Key) für Maschinen-Endpunkte

Jüngste Änderungen dieser Session:
- Belegspezifische Kontaktdaten: alle ERP-Adressen (Beleg-/Liefer-/
  Rechnungsadresse, Ansprechpartner, Kundenstamm) mit Telefon/Mobil/
  E-Mail werden gesynct (Migration 0029, MSSQL-Query, TourDetails)
- Konfiguration von .env (envy/dotenvy) auf config.toml (toml/serde)
  umgestellt; Vorlage config.example.toml, Pfad via HOLZLEITNER_CONFIG

Nicht im Repo (per .gitignore): config.toml (Secrets), data/ (Laufzeit-/
Kundendaten), demo.mp4, .claude/, variocontrol-ai/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:52:58 +02:00

89 lines
2.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use axum::Router;
use axum::extract::{Path, Query, State};
use axum::http::header;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use serde::Deserialize;
use uuid::Uuid;
use crate::error::ApiError;
use crate::extractors::AuthenticatedUser;
use crate::state::AppState;
pub fn router() -> Router<AppState> {
Router::new().route("/attachments/{id}", get(get_attachment))
}
/// Größen-/Format-Parameter für das gerenderte Vorschaubild. Alle optional
/// mit sinnvollen Defaults — die App kann pro Anwendungsfall (Thumbnail vs.
/// Vollbild) abweichende Werte anfragen.
#[derive(Debug, Deserialize)]
pub struct PreviewQuery {
#[serde(default = "default_dimension")]
pub w: u32,
#[serde(default = "default_dimension")]
pub h: u32,
#[serde(default = "default_quality")]
pub q: u32,
#[serde(default = "default_ext")]
pub ext: String,
#[serde(default = "default_page")]
pub page: String,
}
fn default_dimension() -> u32 {
1024
}
fn default_quality() -> u32 {
85
}
fn default_ext() -> String {
"jpeg".to_string()
}
fn default_page() -> String {
"1".to_string()
}
/// Liefert ein gerendertes Vorschaubild des Attachments (Bytes), geladen
/// aus DOCUframe. Auflösung/Format über Query-Parameter steuerbar
/// (`?w=&h=&q=&ext=&page=`).
#[utoipa::path(
get,
path = "/attachments/{id}",
tag = "attachments",
params(
("id" = Uuid, Path, description = "Attachment-Id (unsere UUID)"),
("w" = Option<u32>, Query, description = "Breite in Pixeln (Default 1024)"),
("h" = Option<u32>, Query, description = "Höhe in Pixeln (Default 1024)"),
("q" = Option<u32>, Query, description = "Qualität 0–100 (Default 85)"),
("ext" = Option<String>, Query, description = "png|jpeg|jpg|webp|tiff (Default jpeg)"),
("page" = Option<String>, Query, description = "Seitennummer (Default 1)"),
),
responses(
(status = 200, description = "Vorschaubild (Bytes)", content_type = "image/jpeg"),
(status = 401, description = "Authentifizierung fehlgeschlagen"),
(status = 404, description = "Attachment nicht gefunden")
),
security(("bearer_auth" = []))
)]
pub async fn get_attachment(
State(state): State<AppState>,
AuthenticatedUser(claims): AuthenticatedUser,
Path(id): Path<Uuid>,
Query(query): Query<PreviewQuery>,
) -> Result<Response, ApiError> {
tracing::info!(actor = claims.personalnummer, %id, "attachment.preview");
// DOCUframe-Parameterschema: width_height_quality_extension.
let parameters = format!("{}_{}_{}_{}", query.w, query.h, query.q, query.ext);
let preview = state
.get_attachment_preview
.execute(id, parameters, query.page)
.await?;
Ok((
[(header::CONTENT_TYPE, preview.content_type)],
preview.bytes,
)
.into_response())
}