fix: SPA deep-link assets and broken home screen CSS
Three root causes addressed: 1. Added <base href="/"> to index.html so all relative asset paths (css/app.css, js/*.js) resolve from the root regardless of the current SPA route. Without this, /instance/117 requested /instance/css/app.css, which hit the SPA fallback and returned HTML; helmet's nosniff then refused it as a stylesheet. 2. Removed upgrade-insecure-requests from the CSP (useDefaults: false). This directive told browsers to upgrade HTTP→HTTPS for every asset request, breaking all resource loading on HTTP-only deployments. 3. Changed script-src-attr from 'none' to 'unsafe-inline' to allow the inline onclick handlers used throughout the UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="/">
|
||||
<title>Catalyst</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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('<base href="/">')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user