mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-16 21:00:03 +02:00
fix(maps): serve map assets with CORS
origin: ['*'] in config/cors.ts never matches a real Origin header, since @adonisjs/cors only treats '*' as a wildcard when it's the bare string, not inside an array. So no Access-Control-Allow-Origin is ever sent, on any route, blocking any other app from reading NOMAD's map tiles cross-origin. Add a middleware scoped to the four map asset path prefixes instead of widening the global config, which would also expose the write API since credentials: true is already set there. Registered before @adonisjs/cors so it can answer OPTIONS preflights on map paths itself; the built-in middleware ends any preflight it can't match an origin for before this one would otherwise run.
This commit is contained in:
parent
54ee30a449
commit
2703057f37
34
admin/app/middleware/maps_cors_middleware.ts
Normal file
34
admin/app/middleware/maps_cors_middleware.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { HttpContext } from '@adonisjs/core/http'
|
||||
import type { NextFn } from '@adonisjs/core/types/http'
|
||||
|
||||
// origin: ['*'] in config/cors.ts never matches a real Origin header
|
||||
// (@adonisjs/cors only wildcards on the bare string origin: '*'), and a
|
||||
// global fix would also expose the write API since credentials: true is
|
||||
// set. Scope anonymous, read-only CORS to the map asset paths instead.
|
||||
|
||||
const MAP_ASSET_PREFIXES = ['/api/maps/', '/pmtiles/', '/basemaps-assets/', '/storage/maps/']
|
||||
|
||||
export function isMapAssetPath(pathname: string): boolean {
|
||||
return MAP_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix))
|
||||
}
|
||||
|
||||
export default class MapsCorsMiddleware {
|
||||
async handle({ request, response }: HttpContext, next: NextFn) {
|
||||
if (!isMapAssetPath(request.url())) return next()
|
||||
|
||||
// safeHeader so this stays inert if the global CORS config is ever fixed.
|
||||
response.safeHeader('Access-Control-Allow-Origin', '*')
|
||||
response.header('Access-Control-Expose-Headers', 'ETag, Content-Range, Accept-Ranges, Content-Length')
|
||||
response.header('Vary', 'Origin')
|
||||
|
||||
if (request.method() === 'OPTIONS') {
|
||||
// serve-static only handles GET/HEAD, so OPTIONS would otherwise 404.
|
||||
response.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
|
||||
response.header('Access-Control-Allow-Headers', 'Range, If-None-Match, If-Match')
|
||||
response.header('Access-Control-Max-Age', '600')
|
||||
return response.status(204)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@ server.errorHandler(() => import('#exceptions/handler'))
|
|||
*/
|
||||
server.use([
|
||||
() => import('#middleware/container_bindings_middleware'),
|
||||
// Must precede @adonisjs/cors: it ends unmatched-origin preflights (#endPreFlight)
|
||||
// before this middleware would ever see them, since origin: ['*'] never matches.
|
||||
() => import('#middleware/maps_cors_middleware'),
|
||||
() => import('@adonisjs/cors/cors_middleware'),
|
||||
() => import('@adonisjs/vite/vite_middleware'),
|
||||
() => import('@adonisjs/inertia/inertia_middleware'),
|
||||
|
|
|
|||
23
admin/tests/unit/maps_cors.spec.ts
Normal file
23
admin/tests/unit/maps_cors.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { isMapAssetPath } from '../../app/middleware/maps_cors_middleware.js'
|
||||
|
||||
test('isMapAssetPath accepts map style, pmtiles, and basemaps-assets paths', () => {
|
||||
for (const path of [
|
||||
'/api/maps/styles',
|
||||
'/api/maps/regions',
|
||||
'/pmtiles/washington.pmtiles',
|
||||
'/basemaps-assets/fonts/Noto%20Sans%20Regular/0-255.pbf',
|
||||
'/basemaps-assets/sprites/v4/light.json',
|
||||
'/storage/maps/pmtiles/washington.pmtiles',
|
||||
]) {
|
||||
assert.equal(isMapAssetPath(path), true, `${path} should be a map asset path`)
|
||||
}
|
||||
})
|
||||
|
||||
test('isMapAssetPath rejects non-map paths, including a near-miss without the trailing slash', () => {
|
||||
for (const path of ['/api/system/info', '/api/maps', '/api/mapsfoo', '/maps', '/', '/pmtiles', '/basemaps-assets']) {
|
||||
assert.equal(isMapAssetPath(path), false, `${path} should not be a map asset path`)
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user