judu/ex-egopublic Fork
⑂ 4328d27
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(statics): assets versionnés par contenu (?v=hash), cache immuable, HTML no-storeUnverified

Julien Durillon committed 2026-08-07T23:18:17+02:00 Browse files
4328d27 parent: 23ab955
modified src/routes/mod.rs +11 -0
@@ -69,7 +69,18 @@ pub fn router(state: AppState) -> Router {
6969 /// dans une iframe et de lire ses ressources depuis un autre site.
7070 async fn security_headers(req: Request, next: Next) -> Response {
7171 let mut res = next.run(req).await;
72+ // Les pages (dynamiques, jetons CSRF) ne doivent jamais être mises en
73+ // cache — c'est aussi ce qui garantit qu'une PWA recharge un HTML frais
74+ // pointant vers les assets versionnés après un déploiement.
75+ let html = res
76+ .headers()
77+ .get("content-type")
78+ .and_then(|v| v.to_str().ok())
79+ .is_some_and(|v| v.starts_with("text/html"));
7280 let headers = res.headers_mut();
81+ if html {
82+ headers.insert("cache-control", HeaderValue::from_static("no-store"));
83+ }
7384 headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
7485 // frame-ancestors seulement : une CSP complète casserait le <script>
7586 // inline d'add_step2 et les styles inline.
@@ -69,7 +69,18 @@ pub fn router(state: AppState) -> Router {
69 /// dans une iframe et de lire ses ressources depuis un autre site.69 /// dans une iframe et de lire ses ressources depuis un autre site.
70 async fn security_headers(req: Request, next: Next) -> Response {70 async fn security_headers(req: Request, next: Next) -> Response {
71 let mut res = next.run(req).await;71 let mut res = next.run(req).await;
72+ // Les pages (dynamiques, jetons CSRF) ne doivent jamais être mises en
73+ // cache — c'est aussi ce qui garantit qu'une PWA recharge un HTML frais
74+ // pointant vers les assets versionnés après un déploiement.
75+ let html = res
76+ .headers()
77+ .get("content-type")
78+ .and_then(|v| v.to_str().ok())
79+ .is_some_and(|v| v.starts_with("text/html"));
72 let headers = res.headers_mut();80 let headers = res.headers_mut();
81+ if html {
82+ headers.insert("cache-control", HeaderValue::from_static("no-store"));
83+ }
73 headers.insert("x-frame-options", HeaderValue::from_static("DENY"));84 headers.insert("x-frame-options", HeaderValue::from_static("DENY"));
74 // frame-ancestors seulement : une CSP complète casserait le <script>85 // frame-ancestors seulement : une CSP complète casserait le <script>
75 // inline d'add_step2 et les styles inline.86 // inline d'add_step2 et les styles inline.
modified src/routes/statics.rs +34 -7
@@ -1,12 +1,42 @@
1+use std::hash::Hasher;
2+use std::sync::LazyLock;
3+
14 use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};
25 use axum::response::IntoResponse;
36
4-const CACHE: &str = "public, max-age=86400";
7+const CSS: &[u8] = include_bytes!("../../assets/style.css");
8+const MANIFEST: &[u8] = include_bytes!("../../assets/manifest.json");
9+const ICON: &[u8] = include_bytes!("../../assets/icon-512.png");
10+
11+/// Les URLs sont versionnées par le contenu (`?v=<hash>`) : le cache peut
12+/// être long et immuable, un déploiement qui change l'asset change l'URL.
13+const CACHE: &str = "public, max-age=31536000, immutable";
14+
15+fn versioned(path: &str, bytes: &[u8]) -> String {
16+ let mut h = std::collections::hash_map::DefaultHasher::new();
17+ h.write(bytes);
18+ format!("{path}?v={:08x}", h.finish() as u32)
19+}
20+
21+pub fn css_href() -> &'static str {
22+ static HREF: LazyLock<String> = LazyLock::new(|| versioned("/static/style.css", CSS));
23+ &HREF
24+}
25+
26+pub fn manifest_href() -> &'static str {
27+ static HREF: LazyLock<String> = LazyLock::new(|| versioned("/manifest.json", MANIFEST));
28+ &HREF
29+}
30+
31+pub fn icon_href() -> &'static str {
32+ static HREF: LazyLock<String> = LazyLock::new(|| versioned("/icon-512.png", ICON));
33+ &HREF
34+}
535
636 pub async fn css() -> impl IntoResponse {
737 (
838 [(CONTENT_TYPE, "text/css; charset=utf-8"), (CACHE_CONTROL, CACHE)],
9- include_bytes!("../../assets/style.css").as_slice(),
39+ CSS,
1040 )
1141 }
1242
@@ -16,13 +46,10 @@ pub async fn manifest() -> impl IntoResponse {
1646 (CONTENT_TYPE, "application/manifest+json"),
1747 (CACHE_CONTROL, CACHE),
1848 ],
19- include_bytes!("../../assets/manifest.json").as_slice(),
49+ MANIFEST,
2050 )
2151 }
2252
2353 pub async fn icon() -> impl IntoResponse {
24- (
25- [(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, CACHE)],
26- include_bytes!("../../assets/icon-512.png").as_slice(),
27- )
54+ ([(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, CACHE)], ICON)
2855 }
@@ -1,12 +1,42 @@
1+use std::hash::Hasher;
2+use std::sync::LazyLock;
3+
1 use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};4 use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};
2 use axum::response::IntoResponse;5 use axum::response::IntoResponse;
3 6
4-const CACHE: &str = "public, max-age=86400";7+const CSS: &[u8] = include_bytes!("../../assets/style.css");
8+const MANIFEST: &[u8] = include_bytes!("../../assets/manifest.json");
9+const ICON: &[u8] = include_bytes!("../../assets/icon-512.png");
10+
11+/// Les URLs sont versionnées par le contenu (`?v=<hash>`) : le cache peut
12+/// être long et immuable, un déploiement qui change l'asset change l'URL.
13+const CACHE: &str = "public, max-age=31536000, immutable";
14+
15+fn versioned(path: &str, bytes: &[u8]) -> String {
16+ let mut h = std::collections::hash_map::DefaultHasher::new();
17+ h.write(bytes);
18+ format!("{path}?v={:08x}", h.finish() as u32)
19+}
20+
21+pub fn css_href() -> &'static str {
22+ static HREF: LazyLock<String> = LazyLock::new(|| versioned("/static/style.css", CSS));
23+ &HREF
24+}
25+
26+pub fn manifest_href() -> &'static str {
27+ static HREF: LazyLock<String> = LazyLock::new(|| versioned("/manifest.json", MANIFEST));
28+ &HREF
29+}
30+
31+pub fn icon_href() -> &'static str {
32+ static HREF: LazyLock<String> = LazyLock::new(|| versioned("/icon-512.png", ICON));
33+ &HREF
34+}
5 35
6 pub async fn css() -> impl IntoResponse {36 pub async fn css() -> impl IntoResponse {
7 (37 (
8 [(CONTENT_TYPE, "text/css; charset=utf-8"), (CACHE_CONTROL, CACHE)],38 [(CONTENT_TYPE, "text/css; charset=utf-8"), (CACHE_CONTROL, CACHE)],
9- include_bytes!("../../assets/style.css").as_slice(),39+ CSS,
10 )40 )
11 }41 }
12 42
@@ -16,13 +46,10 @@ pub async fn manifest() -> impl IntoResponse {
16 (CONTENT_TYPE, "application/manifest+json"),46 (CONTENT_TYPE, "application/manifest+json"),
17 (CACHE_CONTROL, CACHE),47 (CACHE_CONTROL, CACHE),
18 ],48 ],
19- include_bytes!("../../assets/manifest.json").as_slice(),49+ MANIFEST,
20 )50 )
21 }51 }
22 52
23 pub async fn icon() -> impl IntoResponse {53 pub async fn icon() -> impl IntoResponse {
24- (54+ ([(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, CACHE)], ICON)
25- [(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, CACHE)],
26- include_bytes!("../../assets/icon-512.png").as_slice(),
27- )
28 }55 }
modified templates/base.html +4 -4
@@ -6,10 +6,10 @@
66 <meta name="theme-color" content="#f5ead8">
77 <meta name="apple-mobile-web-app-title" content="time-time">
88 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9-<link rel="manifest" href="/manifest.json">
10-<link rel="icon" href="/icon-512.png">
11-<link rel="apple-touch-icon" href="/icon-512.png">
12-<link rel="stylesheet" href="/static/style.css">
9+<link rel="manifest" href="{{ crate::routes::statics::manifest_href() }}">
10+<link rel="icon" href="{{ crate::routes::statics::icon_href() }}">
11+<link rel="apple-touch-icon" href="{{ crate::routes::statics::icon_href() }}">
12+<link rel="stylesheet" href="{{ crate::routes::statics::css_href() }}">
1313 <title>{% block title %}time-time{% endblock %}</title>
1414 </head>
1515 <body>
@@ -6,10 +6,10 @@
6 <meta name="theme-color" content="#f5ead8">6 <meta name="theme-color" content="#f5ead8">
7 <meta name="apple-mobile-web-app-title" content="time-time">7 <meta name="apple-mobile-web-app-title" content="time-time">
8 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">8 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9-<link rel="manifest" href="/manifest.json">9+<link rel="manifest" href="{{ crate::routes::statics::manifest_href() }}">
10-<link rel="icon" href="/icon-512.png">10+<link rel="icon" href="{{ crate::routes::statics::icon_href() }}">
11-<link rel="apple-touch-icon" href="/icon-512.png">11+<link rel="apple-touch-icon" href="{{ crate::routes::statics::icon_href() }}">
12-<link rel="stylesheet" href="/static/style.css">12+<link rel="stylesheet" href="{{ crate::routes::statics::css_href() }}">
13 <title>{% block title %}time-time{% endblock %}</title>13 <title>{% block title %}time-time{% endblock %}</title>
14 </head>14 </head>
15 <body>15 <body>