feat(feedback): attachments upload + admin thumbnails + BFF download proxy #111
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
psa-systems/bunyip!111
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/feedback-attachments-upload-and-download"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BUNYIP-86. Final PR of the three-part feedback parity series. Stacks on BUNYIP-85.
bunyip-api already accepts up to 3 attachments at up to 5 MB each across image/png|jpeg|webp|gif and text/plain (
bunyip-api/src/handlers/feedback.rs:29-35, 103-310) and serves them back viaGET /v1/admin/feedback/{id}/attachments/{attachment_id}(handlers/feedback.rs:583-607, routed at routes/admin.rs:125-128). bunyip-web exposed neither the upload UI nor any way to render or download the files in admin triage.Browser-facing form transport:
bunyip-web/Cargo.toml: enable axum'smultipartfeature. Drop the now-unusedform_urlencodeddep (the BUNYIP-79 urlencoded path is gone).bunyip-web/src/handlers/content.rs::feedback_form: addenctype="multipart/form-data"and a<input type="file" name="attachments" multiple accept="image/png,image/jpeg,image/webp,image/gif,text/plain">block under the message field, with a small hint line ("Up to 3 files, 5 MB each").feedback_post(body: Bytes)forfeedback_post(mut multipart: Multipart). Replaceparse_feedback_formwith an asyncread_feedback_multipartthat mirrors the API's per-form ceilings (3 attachments, 5 MB, allowed MIMEs) upstream of the reqwest hop so users see an inline error banner instead of the API's bare 422 after a long upload. Empty file-input slots (zero-byte browser parts) are skipped, not turned into 0-byte rejections. Filenames are basename-sanitized to match the API's strip.bunyip-web/src/main.rs: raise the axum default 2 MB body limit on/feedbackonly viaroute_layer(DefaultBodyLimit::max(FEEDBACK_BODY_LIMIT_BYTES))(16 MB ceiling = 3 × 5 MB + headroom). Other routes stay on the default.BFF -> API hop:
bunyip-web/src/api/calls.rs: addFeedbackAttachment { filename, mime, bytes }andattachments: Vec<FeedbackAttachment>onFeedbackInput.submit_feedbacknow appends areqwest::multipart::Part::bytes(...).file_name(...).mime_str(...)per file under the part nameattachments(the API identifies file parts byContent-Disposition.filename, not by part name).Admin detail rendering:
bunyip-web/src/api/types.rs: addFeedbackAttachmentMeta { id, filename, mime_type, size_bytes }. Add#[serde(default)] attachments: Vec<...>toAdminFeedbackDetailso an older API that does not emit the field deserializes as an empty list.bunyip-web/src/handlers/admin.rs::feedback_detail_view: render an attachments section under the message when non-empty. Image MIMEs get an inline<img>thumbnail loaded from the BFF proxy URL; non-image (text/plain) stays as a labelled download link. Each row shows filename + human-readable size + MIME.Admin attachment download proxy:
bunyip-web/src/handlers/admin.rs::feedback_attachment: GET/admin/feedback/:id/attachments/:attachment_idmirroring the existingfeedback_exportanddashboard::download_assetpattern. Re-auth viaadmin_guard, forward the session cookie to the API, stream the response body back with the upstream Content-Type and Content-Disposition. 401 forces re-auth; any other non-2xx bounces back to the detail page so the browser never saves an error blob.bunyip-web/src/main.rs: register the route between the:iddetail and:id/respondroutes.just check-containerclean (modulo the pre-existingtest_config_defaultsparallel-env-var flake onmain; 188 other tests pass; fmt + clippy + build clean).#BUNYIP-86
Closes BUNYIP-86. Final PR of the three-part feedback parity series. Stacks on BUNYIP-85. bunyip-api already accepts up to 3 attachments at up to 5 MB each across image/png|jpeg|webp|gif and text/plain (`bunyip-api/src/handlers/feedback.rs:29-35, 103-310`) and serves them back via `GET /v1/admin/feedback/{id}/attachments/{attachment_id}` (handlers/feedback.rs:583-607, routed at routes/admin.rs:125-128). bunyip-web exposed neither the upload UI nor any way to render or download the files in admin triage. Browser-facing form transport: - `bunyip-web/Cargo.toml`: enable axum's `multipart` feature. Drop the now-unused `form_urlencoded` dep (the BUNYIP-79 urlencoded path is gone). - `bunyip-web/src/handlers/content.rs::feedback_form`: add `enctype="multipart/form-data"` and a `<input type="file" name="attachments" multiple accept="image/png,image/jpeg,image/webp,image/gif,text/plain">` block under the message field, with a small hint line ("Up to 3 files, 5 MB each"). - Swap `feedback_post(body: Bytes)` for `feedback_post(mut multipart: Multipart)`. Replace `parse_feedback_form` with an async `read_feedback_multipart` that mirrors the API's per-form ceilings (3 attachments, 5 MB, allowed MIMEs) upstream of the reqwest hop so users see an inline error banner instead of the API's bare 422 after a long upload. Empty file-input slots (zero-byte browser parts) are skipped, not turned into 0-byte rejections. Filenames are basename-sanitized to match the API's strip. - `bunyip-web/src/main.rs`: raise the axum default 2 MB body limit on `/feedback` only via `route_layer(DefaultBodyLimit::max(FEEDBACK_BODY_LIMIT_BYTES))` (16 MB ceiling = 3 × 5 MB + headroom). Other routes stay on the default. BFF -> API hop: - `bunyip-web/src/api/calls.rs`: add `FeedbackAttachment { filename, mime, bytes }` and `attachments: Vec<FeedbackAttachment>` on `FeedbackInput`. `submit_feedback` now appends a `reqwest::multipart::Part::bytes(...).file_name(...).mime_str(...)` per file under the part name `attachments` (the API identifies file parts by `Content-Disposition.filename`, not by part name). Admin detail rendering: - `bunyip-web/src/api/types.rs`: add `FeedbackAttachmentMeta { id, filename, mime_type, size_bytes }`. Add `#[serde(default)] attachments: Vec<...>` to `AdminFeedbackDetail` so an older API that does not emit the field deserializes as an empty list. - `bunyip-web/src/handlers/admin.rs::feedback_detail_view`: render an attachments section under the message when non-empty. Image MIMEs get an inline `<img>` thumbnail loaded from the BFF proxy URL; non-image (`text/plain`) stays as a labelled download link. Each row shows filename + human-readable size + MIME. Admin attachment download proxy: - `bunyip-web/src/handlers/admin.rs::feedback_attachment`: GET `/admin/feedback/:id/attachments/:attachment_id` mirroring the existing `feedback_export` and `dashboard::download_asset` pattern. Re-auth via `admin_guard`, forward the session cookie to the API, stream the response body back with the upstream Content-Type and Content-Disposition. 401 forces re-auth; any other non-2xx bounces back to the detail page so the browser never saves an error blob. - `bunyip-web/src/main.rs`: register the route between the `:id` detail and `:id/respond` routes. `just check-container` clean (modulo the pre-existing `test_config_defaults` parallel-env-var flake on `main`; 188 other tests pass; fmt + clippy + build clean). #BUNYIP-86