1
Fork 0
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:
TheGeneralist 2026-07-11 19:45:13 +02:00
parent 5de9e291b0
commit 8755e2c503
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
14 changed files with 574 additions and 298 deletions

View file

@ -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
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title>
<script type="module" crossorigin src="/assets/index-BlLU3Wcz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEaPUEBm.css">
<script type="module" crossorigin src="/assets/index-DekfKifP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Czw8P_Ze.css">
</head>
<body>
<div id="root"></div>