mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +02:00
feat: entry previews — tweet/thread/article/video/audio/image/iframe/pdf
- PreviewModal: modal overlay with new-tab link (↗) and keyboard close - PreviewPanel: routes by entity_kind + primary_media extension to the correct viewer (tweet/video/audio/pdf/html/image/fallback) - TweetPreview: full X-style tweet, thread, and article renderer with local artifact map for archived media (CDN fallback) - AudioBar: persistent fixed bottom player, triggered via ContextRail Play button; body.has-audio-bar pads content above it - VideoPreview, IframePreview, ImagePreview: inline viewers - PreviewPage: standalone /preview/:archiveId/:entryUid route - ContextRail: Play/Preview buttons; isAudio/isPreviewable detection - App.jsx: preview modal state, currentAudio state, preview route guard, has-audio-bar body class effect - routes.rs: CSP updated (media-src self blob https; frame-ancestors self; Google Fonts + external images/scripts whitelisted) - styles.css: preview modal, tweet-wrap scroll (min-height:0), audio bar body padding, newtab button, preview panel flex layout
This commit is contained in:
parent
5de9e291b0
commit
8755e2c503
14 changed files with 574 additions and 298 deletions
|
|
@ -91,36 +91,61 @@ async fn setup_guard(
|
|||
/// Tower middleware: injects HTTP security response headers on every response.
|
||||
/// HSTS is intentionally omitted — that belongs at the reverse-proxy layer.
|
||||
async fn security_headers(req: Request, next: Next) -> Response {
|
||||
// Capture path before consuming req for next.run()
|
||||
let is_artifact = req.uri().path().contains("/artifacts/");
|
||||
let mut response = next.run(req).await;
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("x-content-type-options"),
|
||||
axum::http::HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("x-frame-options"),
|
||||
axum::http::HeaderValue::from_static("DENY"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("referrer-policy"),
|
||||
axum::http::HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("content-security-policy"),
|
||||
axum::http::HeaderValue::from_static(
|
||||
"default-src 'self'; \
|
||||
script-src 'self'; \
|
||||
style-src 'self' 'unsafe-inline'; \
|
||||
img-src 'self' data: blob:; \
|
||||
font-src 'self'; \
|
||||
connect-src 'self'; \
|
||||
frame-ancestors 'none'",
|
||||
),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("permissions-policy"),
|
||||
axum::http::HeaderValue::from_static("camera=(), microphone=(), geolocation=()"),
|
||||
axum::http::HeaderValue::from_static("camera=(), microphone=(), geolocation=(), autoplay=()"),
|
||||
);
|
||||
if is_artifact {
|
||||
// Artifact responses are iframed by the preview modal (sandboxed, no allow-scripts).
|
||||
// When opened directly in a new tab scripts must still be blocked so archived
|
||||
// pages cannot make same-origin API calls with the user's session.
|
||||
// Only styles, images, fonts and media need to be relaxed for rendering.
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("content-security-policy"),
|
||||
axum::http::HeaderValue::from_static(
|
||||
"default-src 'none'; \
|
||||
script-src 'none'; \
|
||||
style-src 'self' 'unsafe-inline' https:; \
|
||||
img-src 'self' data: blob: https:; \
|
||||
font-src 'self' https:; \
|
||||
media-src 'self' blob:; \
|
||||
connect-src 'none'; \
|
||||
frame-ancestors 'self'",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("x-frame-options"),
|
||||
axum::http::HeaderValue::from_static("DENY"),
|
||||
);
|
||||
// Main app CSP — allow Google Fonts and external images for tweet previews
|
||||
headers.insert(
|
||||
axum::http::header::HeaderName::from_static("content-security-policy"),
|
||||
axum::http::HeaderValue::from_static(
|
||||
"default-src 'self'; \
|
||||
script-src 'self'; \
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
|
||||
img-src 'self' data: blob: https:; \
|
||||
font-src 'self' https://fonts.gstatic.com; \
|
||||
media-src 'self' blob: https:; \
|
||||
connect-src 'self'; \
|
||||
frame-src 'self'; \
|
||||
frame-ancestors 'none'",
|
||||
),
|
||||
);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue