diff --git a/index.html b/index.html
index 485c45c..ab3c809 100644
--- a/index.html
+++ b/index.html
@@ -3,6 +3,7 @@
+
Catalyst
diff --git a/server/server.js b/server/server.js
index b14ab0f..5f83e90 100644
--- a/server/server.js
+++ b/server/server.js
@@ -11,10 +11,18 @@ export const app = express();
app.use(helmet({
contentSecurityPolicy: {
+ useDefaults: false, // explicit — upgrade-insecure-requests breaks HTTP deployments
directives: {
- ...helmet.contentSecurityPolicy.getDefaultDirectives(),
- 'style-src': ["'self'", 'https://fonts.googleapis.com'],
- 'font-src': ["'self'", 'https://fonts.gstatic.com'],
+ 'default-src': ["'self'"],
+ 'base-uri': ["'self'"],
+ 'font-src': ["'self'", 'https://fonts.gstatic.com'],
+ 'form-action': ["'self'"],
+ 'frame-ancestors': ["'self'"],
+ 'img-src': ["'self'", 'data:'],
+ 'object-src': ["'none'"],
+ 'script-src': ["'self'"],
+ 'script-src-attr': ["'unsafe-inline'"], // allow onclick handlers
+ 'style-src': ["'self'", 'https://fonts.googleapis.com'],
},
},
}));
diff --git a/tests/api.test.js b/tests/api.test.js
index 79deb87..38138ab 100644
--- a/tests/api.test.js
+++ b/tests/api.test.js
@@ -237,3 +237,43 @@ describe('DELETE /api/instances/:vmid', () => {
expect(res.status).toBe(400)
})
})
+
+// ── Static assets & SPA routing ───────────────────────────────────────────────
+
+describe('static assets and SPA routing', () => {
+ it('serves index.html at root', async () => {
+ const res = await request(app).get('/')
+ expect(res.status).toBe(200)
+ expect(res.headers['content-type']).toMatch(/html/)
+ })
+
+ it('serves index.html for deep SPA routes (e.g. /instance/117)', async () => {
+ const res = await request(app).get('/instance/117')
+ expect(res.status).toBe(200)
+ expect(res.headers['content-type']).toMatch(/html/)
+ })
+
+ it('serves CSS with correct content-type (not sniffed as HTML)', async () => {
+ const res = await request(app).get('/css/app.css')
+ expect(res.status).toBe(200)
+ expect(res.headers['content-type']).toMatch(/text\/css/)
+ })
+
+ it('does not set upgrade-insecure-requests in CSP (HTTP deployments must work)', async () => {
+ const res = await request(app).get('/')
+ const csp = res.headers['content-security-policy'] ?? ''
+ expect(csp).not.toContain('upgrade-insecure-requests')
+ })
+
+ it('allows inline event handlers in CSP (onclick attributes)', async () => {
+ const res = await request(app).get('/')
+ const csp = res.headers['content-security-policy'] ?? ''
+ // script-src-attr must not be 'none' — that blocks onclick handlers
+ expect(csp).not.toContain("script-src-attr 'none'")
+ })
+
+ it('index.html contains base href / for correct asset resolution on deep routes', async () => {
+ const res = await request(app).get('/')
+ expect(res.text).toContain('')
+ })
+})